初识C++

🌈初识C++

一段C++版的hello world

C++是在C的基础之上,容纳进去了面向对象编程思想,增加了许多有用的库,也弥补了许多C语言的不足。

🌈命名空间

来解决C语言明明冲突的问题。

☀️C语言的命名冲突问题

当在C语言中定义一个名叫rand的变量,可能程序会出错,因为在stdlib.h库中,rand是一个函数,命名冲突了:

☀️C++命名空间与域作用限定符

🎈1.对命名空间内变量的访问

定义一个名叫bit的命名空间,该空间内部有一个叫rand的变量,当我想使用该变量时,就用"空间名+域作用限定符+变量名"的方式",即"bit :: rand",当rand前面什么都没有时,默认从全局变量中找rand,即rand函数。

🎈2.命名空间内可以放变量、函数、结构体等,都用 : : 符号来限定。

🎈3.命名空间的内部嵌套

如果同一个命名空间内部两名称冲突,那就在该空间内继续嵌套空间,隔开冲突的名称。

假设大空间bit内部包含了两个小空间bit1和bit2,此时对小空间的访问方式是"大空间+小空间+域作用限定符+变量名",对bit1空间内访问是"bit : : bit1: : rand"。

🎈4.合并命名空间

同一个文件的多个位置出现同一个命名空间,或多个文件中出现同一个命名空间,编译器会将他们合并。

例如:Stack.h文件的bit空间内声明了两个函数,Stack.cpp文件中的bit空间内是这两个函数的定义,Test.cpp文件中用域作用限定符对函数进行访问。由于编译器的合并命名空间功能,从而也可以实现分文件操作。


🎈5.默认指定展开

(1)整体展开

即默认指定全部命名空间。

如果要用bit空间内的变量、函数等,还要每次都要指定bit空间,即写"bit : : (...)"太麻烦了,而如果设置默认访问bit空间,即默认使用的每一个函数或者变量都是bit空间中的,就不需要麻烦的写"bit : : (...)"了。

指定命名空间语法:using namespace (空间名)

例:展开指定C++标准库定义的命名空间std:

注:工程项目不要展开std,容易冲突,日常练习可以。最安全的方式是指定展开我们自己创建的命名空间,需要哪个展开哪个。

(2)部分展开

即默认指定命名空间内部分的变量、函数等。全部展开风险太大,部分展开可以规避风险。

cout是C++的输出流,cin是输入流,<<是流插入运算符,>>是流提取元运算符,都储存在空间std中。当我们不全部展开,只展开部分常用的,就可以在规避风险的同时实现便利。

例:部分展开前后对比:

当std不是默认指定空间时,用cout输出变量a和b:

太麻烦,指定cout为默认访问:

🌈C++输入&输出

cout是输出(流输出),cin是输入(流提取),cout、cin都是定义在头文件iostream中的。

☀️使用cout和cin打印与输入数据

将std库中的cout设为默认后,直接使用:

☀️C++可以兼容C

目前还不知道怎样用C++控制数据精度,可以借助C语言打印精度更高的数据:

🌈缺省参数

☀️缺省参数概念:

缺省参数是声明或定义函数时为函数的参数指定一个缺省值。调用时可传参也可不传参,不传参时,参数值就是指定的缺省值。缺省值必须是常量或者全局变量。

☀️缺省参数的声明与定义

当函数既有声明又有定义时,缺省参数不可以在声明和定义中都出现,防止两个参数值不一样。也不能只在定义中出现声明中不出现,这样包含头文件时会不知道声明中的参数是几。只能单独在声明中出现。

.h文件中的声明里需要出现缺省值:

.cpp文件中的定义里不可出现缺省值:

☀️缺省参数的分类

🎈1.全缺省参数

所有参数都是缺省参数,声明时需要指定默认值:

c 复制代码
void Func(int a = 10, int b = 20, int c = 30)
 {
     cout<<"a = "<<a<<endl;
     cout<<"b = "<<b<<endl;
     cout<<"c = "<<c<<endl;
 }

🎈2.半缺省参数

只有后半部分参数是缺省参数:

c 复制代码
void Func(int a, int b = 10, int c = 20)
 {
     cout<<"a = "<<a<<endl;
     cout<<"b = "<<b<<endl;
     cout<<"c = "<<c<<endl;
 }

注:半缺省参数必须从右往左依次来给出,不能从左往右给,也不能间隔着给。(如果从前往后设置缺省参数或者间隔着设置,那么编译器会不知道哪个对应的是缺省参数)

☀️缺省参数应用

比如在栈初始化申请空间时,不论知不知道要多大的空间,都可以用同一个函数。具体方法是使用缺省参数设置缺省值,该值表示默认开辟的空间大小,知道需要多少空间就传参,不知道就不传参使用缺省值。

🌈函数重载

☀️函数重载概念:

C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。

注:返回值可同可不同,具体取决于返回的数据的类型。

类型不同:

个数不同:

☀️注:函数重载要避免歧义

Add有两个重载函数,第一个的两个参数都是int类型的,第二个的两个参数都是double类型的:

此时,如果一个函数调用为Add(1,2),则会匹配到Add(int left,int right);如果一个函数调用为Add(1.1,2.2),则会匹配到Add(double left,double right)。但是当函数调用为Add(1,2.2)时,既可以匹配到Add(int left,int right)(此时会将第二个参数转换成int类型),也可以匹配到Add(double left,double right)(此时将第一个参数转换成double类型),有了歧义,这时编译器不知道匹配哪一个函数,会报错。

再比如,函数f有两个重载函数,第一个无参数,第二个有一个缺省参数:

c 复制代码
void f()
{
 cout << "f()" << endl;
}
void f(int a=1)
{
 cout << "f(int a)" << endl;
}

此时,如果不传参调用f函数,可以匹配到第一个无参数的f函数,也可以匹配到第二个不传值情况下的缺省函数,产生歧义,编译器报错。不传参时调用存在二义性:

🌈引用(🌟🌟🌟)

☀️一、引用概念:

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。

比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"。那么电脑中的同一块空间就有了三个名字:李逵、铁牛、黑旋风,通过这三个名字都可以找到这块空间。

同一个变量可以有多个别名,即多次引用:

☀️二、C的指针&C++的引用

C的传址调用:调用时需要传地址,函数内部接收到后需要解引用

C++的引用:调用时直接用变量名,函数内部以取别名的方式接收变量

第一个Swap是C语言版本的传地址,第二个Swap是C++版本的引用。第二个Swap函数表示,left是a的别名,right是b的别名,left改变则a改变,right改变则b改变。

🎈1.引用是指针的便捷方式

(1)分析C语言传址调用的不便:

不便一:传值调用只改变形参不改变实参,无法达到对数据的修改。

不便二:传几级指针就需要几级的解引用,如果指针的级数和解引用的级数不匹配,则会出错,而多级指针很容易出现不匹配的错误。

以顺序表传头节点指针为例:

节点的内容存放在结构体SLTNode中 -> 通过结构体指针访问节点 -> 头节点指针是一个一级结构体指针 -> 传参时为了能进入指针内部,需要传一级指针的地址,即二级指针 -> 接收时用二级指针接收

错误的调用方式:

phead是plist的拷贝,形参的改变不影响实参,phead=newnode无法让plist也等于newnode。

正确调用方法:

只有通过取plist的地址,再对该地址解引用,才能让phead的值改变。

(2)用C++的引用解决不便
🌟插入新知识:typedef结构体指针名

当对结构体重命名时,如果名称前有*符号,则该名称代表指向该结构体变量的指针名称。比如:

PSLTNode是结构体指针名,PSLTNode = &SLTNode。

--- --- --- --- --- --- --- --- ---👻分割线👻 --- --- --- --- --- --- --- --- ---

使用引用方式,传参时传结构体指针名,接收参数时用一个别名接收,就可大大简化传地址的不便:

节点名称:STLNode

节点指针:STLNode* 。STLNode*=PSTLNode

(直接在节点的基础上对结点指针重命名)

在之前的基础上有两方面的改动:

1.将所有STLNode*替换成PSTLNode

2.用引用的方式将结点指针类型的变量起别名为phead,即用&PSTLNode phead的方式接收传来的参数。

🎈2.指针不可被引用替代的方面

(1)引用必须初始化,指针可以是NULL

引用必须初始化,就是说明起的是谁的别名。不能人还没有呢就先来个别名。

(2)引用不可以改变指向,指针可以

值变,地址不变,说明没有改变c的指向,只改变了c里面的值。只能给A增加或更换别名,不能把A的别名拿着说这是B的别名。

由于C++无法改变指向即地址,因此数据结构的某些部位如链表还是要用指针。比如在链表中插入一个节点,此时需要改变指针的指向,无法用引用的方式完成。

🎈3.总结引用和指针的异同点

(1)相同点

引用和指针的底层都开辟空间了。

(2)不同点

1.引用概念上定义一个变量的别名,指针存储一个变量地址。

  1. 引用在定义时必须初始化,指针没有要求。

  2. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。即引用不可以改变指向,指针可以。

  3. 没有NULL引用,但有NULL指针。

  4. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)。

  5. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。

  6. 有多级指针,但是没有多级引用。

  7. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理。

  8. 引用比指针使用起来相对更安全。

☀️三、引用作参数、引用作返回值

🎈1.做参数,方便好理解

🎈2.做返回值

(1)引用返回的风险

引用返回的本质是野指针:

返回值原本在函数的栈帧中有一席之地,但函数运行完被销毁了,返回值所在的空间也被销毁了(被销毁不是说空间消失了或不能用了,意思是这片空间不受管辖,可以随意被分配给其他地方装载其他值)。引用返回,返回的是变量n的别名,通过该返回值的别名找到这块不受管辖的空间,就如同野指针。

对比传值返回:函数调用结束,返回的变量也跟着销毁,所以实际返回的不是变量本身,而是变量的拷贝。

说明在vs下,销毁栈帧不会换成随机值,在被其他值覆盖前保持之前函数在此处放的值。其他编译器下不一定。

(2)引用返回的正确用法

在被返回的值前加static,被static修饰的局部变量只会被初始化一次。

①此时的ret已经和Add(1,2)的返回值绑定:

c 复制代码
#include <iostream>
using std::cout;
using std::endl;
int& Add(int a, int b) {
	static int c = a + b;
	return c;
}
int main() {
	int& ret = Add(1, 2);
	cout << "Add(1,2) is " << ret << endl;
	Add(3, 4);
	cout << "Add(3,4) is " << ret << endl;
}

②在(1)的 基础上实现返回值的变动,此时ret和c这块空间内放的任何数据都绑定:

c 复制代码
#include <iostream>
using std::cout;
using std::endl;
int& Add(int a, int b) {
	static int c ;
	c = a + b;
	return c;
}
int main() {
	int& ret = Add(1, 2);
	cout << "Add(1,2) is " << ret << endl;
	Add(3, 4);
	cout << "Add(3,4) is " << ret << endl;
}

☀️四、传值、传引用效率比较

🎈 1.传值调用&传引用调用

测试对比传值调用和传引用调用的时间差异:

c 复制代码
#include <iostream>
using std::cout;
using std::endl;
#include <time.h>
struct A { int a[10000]; };
void TestFunc1(struct A a) {}
void TestFunc2(struct A& a) {}
//也可以不加struct写成:void TestFunc1(A a) {}
//                    void TestFunc2(A& a) {}
void TestRefAndValue()
{
	struct A a;
	// 以值作为函数参数
	size_t begin1 = clock();
	for (size_t i = 0; i < 10000; ++i)
		TestFunc1(a);
	size_t end1 = clock();
	// 以引用作为函数参数
	size_t begin2 = clock();
	for (size_t i = 0; i < 10000; ++i)
		TestFunc2(a);
	size_t end2 = clock();
	// 分别计算两个函数运行结束后的时间
	cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
	cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main() {
	TestRefAndValue();
	return 0;
}

结论:传引用调用效率高。

🎈2.传值返回&传引用返回

c 复制代码
#include <iostream>
using std::cout;
using std::endl;
#include <time.h>
struct A { int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a; }
// 引用返回
A& TestFunc2() { return a; }
void TestReturnByRefOrValue()
{
	// 以值作为函数的返回值类型
	size_t begin1 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc1();
	size_t end1 = clock();
	// 以引用作为函数的返回值类型
	size_t begin2 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc2();
	size_t end2 = clock();
	// 计算两个函数运算完成之后的时间
	cout << "TestFunc1 time:" << end1 - begin1 << endl;
	cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main() {
	TestReturnByRefOrValue();
	return 0;
}

结论:传引用返回效率高得多。

🎈3.引用返回可做左值,方便修改返回对象

传值返回时,返回的是一个拷贝的值,这个值只能放在等号右边,即必须要用一个变量接收这个返回值;引用返回,返回的是下标为pos的那个位置的别名,可以充当等好的左边,可以被改变。因此对这个位置赋值或修改是很方便的(比指针方便,指针还要解引用呢)。

☀️五、常引用

🎈1.常引用概念:

简单来说就是给变量或者常量取带有常属性的别名,具有了常属性就不可被修改了。

一个变量前加上const后就具有了常属性。从变量到常量属于权限的缩小;从常量到常量属于权限的平移;从常量到变量属于权限的放大。

🌟复习一下宏:

1.概念:宏就是一种单纯的替换,没有类型,可以替换函数表达式、变量常量等;宏后面不可加分号;宏会被替换成表达式。

2.注:如果宏用来替换一个表达式,则对每一项运算对象都加括号,防止计算对象本身就是表达式。例如:

如果宏定义的ADD是:ADD(x,y) (x+y),则容易出现运算符优先级的错误。

2.两种常引用操作

(1)对无常属性变量常引用
c 复制代码
int a = 10;
const int& ra = a;

ra是a的别名,同时ra具有常属性,从a到ra属于权限的缩小。

(2)对有常属性变量常引用
c 复制代码
const int a = 10;
const int& ra = a;

ra是a的别名,a本身就具有常属性,ra也有常属性,从a到ra属于权限的平移。

❌易犯错误:给具有常属性的变量起没有常属性的别名,属于放大权限:

c 复制代码
const int a = 10;
int& ra = a;
(3)对常量常引用
c 复制代码
const int& b = 10;

10本身是一个常量,b是10的别名,因此也必须有常属性,从10到b属于权限的平移。

❌易犯错误:给常量起没有常属性的别名,属于放大权限:

c 复制代码
int& b = 10;
(4)别名的数据类型不同
c 复制代码
double d = 12.34;
const int& rd = d;

整型可以隐式转换成double类型,内部过程是整型的i先存进一个临时变量中转换成double,再存储到double类型的变量j中,由于临时变量具有常属性,因此给double类型的引用加上const后,就可以存进整型的i了。简单来说,类型不一样,进行转换时,加个const就可以了。

❌易犯错误:

c 复制代码
double d = 12.34;
int& rd = d;

🌈内联函数

☀️内联函数概念:

以inline修饰的函数叫做内联函数,在编译期间编译器会用函数体替换函数的调用。相当于C语言的宏。

注:C++中替代宏的技术:

1.常量定义,换用const enum

  1. 短小函数定义,换用内联函数

☀️内联函数特性

1.inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用。

缺陷:可能会使目标文件变大。

优势:没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。

2.inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。

  1. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。

☀️使用内联函数注意事项

内联只适合小函数(10行以内),当大函数用内联时(假设大函数100行,一共调用量10000次),则展开指令合计100*10000行,不展开指令合计100+10000行。内联函数会影响可执行程序大小,影响更新的大小和时间,无条件地使用内联函数会让程序很庞大。

🌈 auto关键字

变量前加上auto关键字,可以识别出这个是什么类型的变量,从而替代了原先的准确变量类型。

typeid是打印类型函数,typeid(变量名).name()。

注:如果想让auto推导出引用的话,必须写成auto&,此时推导出的类型是int

☀️auto真正的意义:

🎈1.代替长类型,如

变量类型是vector< s t r i n g string string>: :iterator,太长了,用auto简化:

注意:

1.用auto替代类型名的变量必须要在右边给值初始化 :

2.auto不可以作为函数返回值, 因为如果不调用该函数,或者无返回值,此时编译器就不知道推导什么了:

3.auto不可以声明数组:

🎈2.用于遍历数组

(1)C语言通过下标或指针来遍历数组:

(2)C++基于数组遍历:

依次取数组中的数据赋值给e,自动判断结束,自动++往后走。也可以用int,但auto很方便,适用于各个类型的数组。

注:e是对数组数据的拷贝,对e的修改不影响实际数组中的数据,即下面的写法不起作用:

如果想要实现堆数组中数据的改变,则需要用引用的方式让e数组数据共用同一块空间:

注:错误写法:

此时不可以这样写,因为传参传进来的是指针不是数组(以array[ ]接收到的参数是指针,相当于(int* array)),即array在这个函数中是一个指针名而不是数组名,for循环写法只能基于数组。

🌈 NULL和nullptr

☀️NULL:

NULL本质上是一个宏,有些极端情况下被替换成数字0,从而引发麻烦,如果硬要将NULL按照指针方式来使用,必须对其进行强转(void ∗ * ∗)。

☀️nullptr:

C++中空指针换成nullptr,能用NULL的情况下都能用nullptr,而且还不会被替换成0。nullptr等价于(void ∗ * ∗)NULL。

证明:

当传参为NULL时:

(注:函数用于接受参数的变量只有类型无变量名,但不会报错,因为程序不用传进来的值,只要传一个整型数据就行)

证明NULL被当成数字而不是指针。

当传参为nullptr时:

证明nullptr就是指针类型。

相关推荐
SRY122404192 小时前
javaSE面试题
java·开发语言·面试
李元豪3 小时前
【智鹿空间】c++实现了一个简单的链表数据结构 MyList,其中包含基本的 Get 和 Modify 操作,
数据结构·c++·链表
无尽的大道3 小时前
Java 泛型详解:参数化类型的强大之处
java·开发语言
ZIM学编程3 小时前
Java基础Day-Sixteen
java·开发语言·windows
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
UestcXiye3 小时前
《TCP/IP网络编程》学习笔记 | Chapter 9:套接字的多种可选项
c++·计算机网络·ip·tcp
一丝晨光4 小时前
编译器、IDE对C/C++新标准的支持
c语言·开发语言·c++·ide·msvc·visual studio·gcc
阮少年、4 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
代码小鑫4 小时前
A027-基于Spring Boot的农事管理系统
java·开发语言·数据库·spring boot·后端·毕业设计
丶Darling.5 小时前
Day40 | 动态规划 :完全背包应用 组合总和IV(类比爬楼梯)
c++·算法·动态规划·记忆化搜索·回溯