文章目录
【写在前面】
点到为止,并不深入。其次建工程这里就不说了,在之前的基础上 —— 文件名.cpp 就可以了
一、C++关键字 (C++98)
💨 C 语言有 32 个关键字,而 C++ 有 63 个关键字,C 语言的关键字在 C++ 中继续可以使用
ps:在本章中不对关键字进行详讲
❗ I/O ❕
//C++兼容C绝大多数语法 #include<stdio.h> int main01() { printf("Hello CPLUSPLUS\n"); return 0; } //但C++通常会这样写 #include<iostream> using namespace std; int main() { cout << "Hello CPLUSPLUS" << endl; return 0; }
二、命名空间
对比 C 语言,一般 C++ 每增加一个语法都是为了解决一些 C 语言做不到的事或者是 C 语言做的不好的地方
在 C/C++ 中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace 关键字的出现就是针对这种问题的。
❗ C语言命名冲突示例 ❕
在不同的作用域中,可以定义同名的变量;在同一作用域下,不能定义同名的变量
#include<stdio.h> //#include<stdlib.h> int a = 0; int rand = 10; int main() { int a = 1; printf("%d\n", rand); return 0; }
📝 分析:
可以看到我们定义了一个 rand 变量输出是没有问题的,但如果包含了 stdlib 头时就会产生命名冲突,此时我们的 rand 变量就和库里的产生冲突;
实际除此之外在大型项目开发时,还有可能和同事之间发生冲突。C 语言面对这种问题是无法解决的,而对于这种场景 C++ 使用了命名空间
💦 命名空间定义
定义命名空间,需要使用到 namespace 关键字,后面跟命名空间的名字,然后接一对 {} 即可,{} 中即为命名空间的成员
⚠ 注意一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中
//1. 普通的命名空间 namespace N1 // N1为命名空间的名称 { //命名空间中的内容,既可以定义变量,也可以定义函数 int a; int Add(int left, int right) { return left + right; } } //2. 命名空间可以嵌套 namespace N2 { int a; int b; int Add(int left, int right) { return left + right; } namespace N3 { int c; int d; int Sub(int left, int right) { return left - right; } } } //3. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中 namespace N1 { int a = 10; } namespace N1 { int b = 20; }
❗ 用命名空间解决变量 rand 和 stdlib 库里的命名冲突 ❕
#include<stdio.h> #include<stdlib.h> namespace WD//定义了一个命名空间域 { int rand = 10;//定义变量 int Add(int x, int y)//定义函数 { return x + y; } struct Node//定义结构体类型 { struct Node* next; int val; }; } int main() { printf("%p\n", rand);//函数指针 printf("%d\n", WD::rand);//rand变量;‘::’叫做域作用限定符 WD::Add(3, 5);//调用函数 struct WD::Node node1;//结构体 return 0; }
❗ 嵌套命名空间 ❕
#include<stdio.h> #include<stdlib.h> namespace WD { int w = 20; int h = 10; namespace WH//嵌套命名空间域 { int w = 10; int h = 20; } } int main() { printf("%d\n", WD::WH::h);//访问嵌套命名空间 return 0; }
❗ 相同名称的命名空间 ❕
namespace WD { int a = 10; int b = 20; namespace WH { int a = 20; int b = 10; } } namespace WD//相同名称的命名空间 { int rand = 50; //int a = 10;//err,在合并的时候冲突了 }
💦 命名空间使用
❓ 如何使用命名空间里的东西 ❔
1️⃣ 全部直接展开到全局
using namespace WD; //using namespace std;//std是包含C++标准库的命名空间
💨优点:用起来方便
💨缺点:自己定义的东西也会暴露,导致命名污染
2️⃣ 访问每个命名空间中的东西时,指定命名空间
std::rand;
💨优点:不存在命名污染
💨缺点:如果要去访问多个命名空间里的东西时,需要一一指定
3️⃣ 仅展开常用的内容
using WD::Node; using WD::Add;
💨优点:不会造成大面积的污染;把常用的展开后,也不需要一一指定
namespace WD { int a = 10; int b = 20; //... } int main() { //using namespace WD;//1.展开WD空间所有内容 //printf("%d\n", WD::a);//2.指定命名空间 //using WD::a;//3.仅展开a }
❓ 上面说了 C++ 把库里的东西都放到 std 这个域里了,那直接展开 std 不就行了或者包头 ❔
注意
1️⃣ #include <iostream>:展开定义
2️⃣ using namespace std;:允许用
所以两者缺一不可
三、C++中的I/O
❗ 说明 ❕
使用 cout 标准输出 (控制台) 和 cin 标准输入 (键盘) 时,必须包含 < iostream> 头文件以及 std 标准命名空间 (std是包含 C++ 标准库的命名空间)
⚠ 注意早期标准库将所有功能在全局域中实现,声明在 .h 后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在 std 命名空间下,为了和 C 头文件区分,也为了正确使用命名空间,规定 C++ 头文件不带 .h ;旧编译器 (vc 6.0) 中还支持 <iostream.h> 格式,后续编译器已不支持,因此推荐使用 < iostream > + std 的方式。
#include<iostream> using namespace std; int main() { cout<<"Hello world!!!"<<endl; return 0; }
❗ 上面这种写法其实有一定的规范缺陷 ❕
C++ 为什么用一个库去包它的命名空间,就是怕我们定义的与库冲突。但是一句 using namespace std; 就把库展开了,那么定义 std 的优势就无了。所以在项目中比较规范的写法如下:
#include<iostream> //展开常用————工程项目中常见的对命名空间的用法 using std::cout; using std::endl; int main() { //只要是库里的都得指定std //std::cout << "Hello world!!!" << std::endl //但如果cout经常要用的话,就在上面单独展开 cout << "Hello world!!!" << endl; return 0; }
⚠ 注意,在日常做题时不需要像项目那样规范,可以直接全部展开 std
❗ cout && cin ❕
cout 和 cin 类似 C 语言的 printf 和 scanf,这里只是先了解下,因为对于 C 语言中的 I/O 是函数,而 C++ 是对象
#include<iostream> using namespace std; int main() { int n; cin >> n;//cin可以理解为键盘;>>可以理解为输入运算符/流提取运算符(官方) int* a = (int*)malloc(sizeof(int) * n); for(int i = 0; i < n; ++i) { cin >> a[i]; } for(int i = 0; i < n; ++i) { cout << a[i] << " ";//cout可以理解为控制台;<<可以理解为输出运算符/流插入运算符(官方) } cout << endl;//等价于count << '\n'; return 0; }
❗ 对于I/O,C++比C便捷 ❕
C++ 不需增加数据格式控制,比如:整形 – %d,字符 – %c,它会自动实别
#include<iostream> using namespace std; int main01() { int n; cin >> n; double* a = (double*)malloc(sizeof(int) * n); for(int i = 0; i < n; ++i) { cin >> a[i];//它会自动实别 } for(int i = 0; i < n; ++i) { cout << a[i] << " ";//它会自动实别 } count << endl; return 0; } //挺爽的吧!!! int main() { char ch = 'C'; int i = 10; int* p = &i; double d = 3.14; double b = 3.1415926; cout << ch << endl; cout << i << endl; cout << p << endl; cout << d << endl; cout << b << endl;//注意对于浮点数,C++最多输出5位,当然也可以指定输出多少位,但是相对指定的方式(比较麻烦)可以配合printf使用 return 0; }
❗ 当然C++也并不完美 ❕
#include<iostream> using namespace std; struct Student { char name[10]; int age; }; int main() { struct Student s = { "小三", 18 }; //输出结构体信息。类似下面场景printf更好 //对于这种情况C++中不用纠结用啥,用习惯自己的就好 cout << "名字:" << s.name << " " << "年龄:" << s.age << endl; printf("名字:%s 年龄:%d\n", s.name, s.age); return 0; }
四、缺省参数
💦 缺省参数概念
缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参
#include<iostream> using namespace std; void TestFunc(int a = 0)//参数缺省值 { cout << a << endl; } int main() { TestFunc();//没有指定实参,使用缺省值 TestFunc(10);//指定实参,使用实参 return 0; }
❗ 有什么用 ❕
比如在 C 语言中有个很苦恼的问题是写栈时,不知道要开多大的空间,之前我们是如果栈为空就先开 4 块空间,之后再以 2 倍走,如果我们明确知道要很大的空间,那么这样就只能一点一点的接近这块空间,就太 low 了。但如果我们使用缺省,明确知道不需要太大时就使用默认的空间大小,明确知道要很大时再传参
#include<iostream> using namespace std; namespace WD { struct Stack { int* a; int size; int capacity; }; } using namespace WD; void StackInit(struct Stack* ps) { ps->a = NULL; ps->capacity = 0; ps->size = 0; } void StackPush(struct Stack* ps, int x) { if(ps->size == ps->capacity) { //ps->capacity *= 2;//err ps->capacity == 0 ? 4 : ps->capacity * 2;//这里就必须写一个三目 } } void StackInitCpp1(struct Stack* ps, int defaultCP) { ps->a = (int*)malloc(sizeof(int) * defaultCP); ps->capacity = 0; ps->size = defaultCP; } void StackInitCpp2(struct Stack* ps, int defaultCP = 4)//ok { ps->a = (int*)malloc(sizeof(int) * defaultCP); ps->capacity = 0; ps->size = defaultCP; } int main() { //假设明确知道这里至少需要100个数据到st1 struct Stack st1; StackInitCpp1(&st1, 100); //假设不知道st2里需要多少个数据 ———— 希望开小点 struct Stack st2; StackInitCpp2(&st1);//缺省 return 0; }
💦 缺省参数分类
❗ 全缺省参数 ❕
void TestFunc(int a = 10, int b = 20, int c = 30) { cout << "a = " << a << endl; cout << "b = " << b << endl; cout << "c = " << c << endl; cout << endl; } int main() { //非常灵活, TestFunc(); TestFunc(1); TestFunc(1, 2); TestFunc(1, 2, 3); //TestFunc(1, , 3);//err,注意它没办法实现b不传,只传a和b,也就是说编译器只能按照顺序传 return 0; }
⚠ 注意:
1️⃣ 全缺省参数只支持顺序传参
❗ 半缺省参数 ❕
//void TestFunc(int a, int b = 10, /*int f, - err*/ int c = 20);//err, void TestFunc(int a, int b = 10, /*int f, int x = y, -> err*/ int c = 20) { cout << "a = " << a << endl; cout << "b = " << b << endl; cout << "c = " << c << endl; cout << endl; } int main() { //TestFunc();//err,至少得传一个,这是根据形参有几个非半缺省参数确定的 TestFunc(1); TestFunc(1, 2); TestFunc(1, 2, 3); return 0; }
//a.h void TestFunc(int a = 10); //a.cpp void TestFunc(int a = 20) {}
⚠ 注意:
1️⃣ 半缺省参数必须从右往左依次来给出,且不能间隔着给
2️⃣ 缺省参数不能在函数声明和定义中同时出现
3️⃣ 缺省值必须是常量或者全局变量
4️⃣ C 语言不支持缺省
五、函数重载
自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。
比如:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前者是 “谁也赢不了!”,后者是 “谁也赢不了!”
💦 函数重载概念
函数重载:是函数的一种特殊情况,C++ 允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表 (参数个数或类型或顺序) 必须不同,常用来处理实现功能类似数据类型不同的问题,显然这是 C 语言做不到的东西
//1.参数个数 int Add(int x) {} int Add(int x, int y) {} //2.参数类型 int Add(int x, int y) {} int Add(int x, double y) {} //3.参数顺序 int Add(int x, float y) {} int Add(float y, int x) {}
❗ 怎么调用 ❕
int Add(int left, int right) { return left + right; } double Add(double left, double right) { return left + right; } long Add(long left, long right) { return left + right; } int main() { //这里分别调用三种不同的函数 Add(10, 20);//默认整型 Add(10.0, 20.0);//浮点型 Add(10L, 20L);//长整型 return 0; }
⚠ 注意
对于函数重载,如果你想调用某一函数,那么在传参的时候就必须写好对应参数的类型、个数、顺序,比如 float 数据,就要写 3f,因为这样它才能找到对应的函数调用
💦 名字修饰 (name Mangling)
❓ 为什么C++支持函数重载,而C语言不支持函数重载呢,以 Linux 环境下演示 ❔
注意这里就不细讲 Linux 的一些指令了,具体的我都写在或者将写在 《Linux专栏》里了
先在 Linux 下以两种编译方式编译函数重载的程序,这里有三个文件 :f.h、f.c、test.c
▶ 可以看到以 C 语言去编译函数重载报错了
▶ 可以看到以 C++ 去编译函数重载是可以的
在正式探究前我们先回忆下,注意 C/C++ 都有类似的过程,但肯定是有区别的,现在我们要找的就是那个区别
这里我们对照着程序走一遍过程
然后再回到我们的问题
❓ C语言不支持函数重载 ❔
C 编译器,直接用函数名关联,函数名相同时,无法区分
❗ 验证 ❕
通过命令 objdump -S 去查看 C 编译生成的符号表
通过命令 objdump -S 去查看 C++ 编译生成的符号表
❓ C++ 支持函数重载 ❔
从上就可以看出对于函数重载 C++ 相对于 C 语言引入了某种规则
C++ 引用了《函数名修饰规则 (Linux下)》不能直接用函数名,要对函数名进行修饰 (带入参数的特点修饰)
📝 说明:
1️⃣ _Z 是前缀
2️⃣ 3 是函数名的长度
3️⃣ Add 是函数名
4️⃣ ii / dd 是函数参数类型的首字母,如果是 int* i,那么就是 pi
💨小结:
C++ 是支持函数重载的,函数名相同,只要参数不同,修饰在符号表中的名字也不同,那么就能区分了
❗ Windows下函数名修饰规则 ❕
🍳【扩展学习:C/C++函数调用约定和名字修饰规则】
❓ 下面两个函数属于函数重载吗 (编译器能不能只实现返回值不同,就能构成重载) ❔
short Add(short left, short right) { return left+right; } int Add(short left, short right) { return left+right; }
显然《函数名修饰规则》并不能让它们支持重载。
❓ 其次如果想自己定义《函数名修饰规则》让只有返回值不同的函数支持重载可以吗 ❔
💨 编译器层面是可以区分的
short Add; -> _Z3sAdd
int Add; -> _Z3iAdd
💨 语法调用层面有严重歧义
Add(3, 5); ???
❓ 下面两个函数能形成函数重载吗?❔
void TestFunc(int a = 10) { cout<<"void TestFunc(int)"<<endl; } void TestFunc(int a) { cout<<"void TestFunc(int)"<<endl; }
虽然两个函数的参数是缺省值和非缺省值,但是依旧不影响修饰出来的函数名,所以不能构成函数重载
💦 extern"C"
有时候在 C++ 工程中可能需要将某些 (部分) 函数按照 C 的风格来编译,在函数前加 extern “C”,意思是告诉编译器,将该函数按照 C 语言规则来编译。比如:tcmalloc 是 google 用 C++ 实现的一个项目,他提供 tcmallc() 和 tcfree两个接口来使用,但如果是 C 项目就没办法使用,那么他就使用 extern “C” 来解决。
extern "C" int Add(int left, int right); int main() { Add(1,2); return 0; }
💨总结
1️⃣ C++ 项目可以调用 C++ 库,也可以调用 C 的库,C++ 是直接兼容 C 的
2️⃣ C 项目可以调用 C 库,也可以使用 extern"C" 调用 C++ 库 (C++ 提供的函数加上 extern"C")
六、引用
💦 引用概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,语法理解上程序不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间
比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"
❗ 类型& 引用变量名(对象名) = 引用实体 ❕
int main() { //有一块空间a,后面给a取了三个别名b、c、d int a = 10; int& b = a; int& c = a; int& d = b; //char& d = a;//err,引用类型和引用实体不是同类型(这里有争议————char a = b[int类型],留个悬念,下面会解答) //会被修改 c = 20; d = 30; return 0; }
⚠ 注意
1️⃣ 引用类型必须和引用实体是同种类型
2️⃣ 注意区分 ‘&’ 取地址符号
💦 引用特性
1️⃣ 引用在定义时必须初始化
2️⃣ 一个变量可以有多个引用
3️⃣ 引用一旦引用一个实体,再不能引用其他实体
int main() { //int& e;//err int a = 10; int& b = a; //这里指的是把c的值赋值于b int c = 20; b = c; return 0; }
💦 常引用
void TestConstRef() { const int a = 10; //int& ra = a; //该语句编译时会出错,a为常量;由const int到int const int& ra = a;//ok int b = 20; const int& c = b; //ok,由int到const int //b可以改,c只能读不能写 b = 30; //c = 30;//err //b、c分别起的别名的权限可以是不变或缩小 int& d = b;//ok //int& e = c//err const int& e = c;//ok //int& f = 10; // 该语句编译时会出错,b为常量 const int& g = 10;//ok int h = 10; double i = h;//ok //double& j = h;//err const double& j = h;//ok //?为啥h能赋值给i了(隐式类型转换),而给h起一个double类型的别名却不行————如果是仅仅是类型的问题那为啥加上const就行了? //double i = h;并不是直接把h给i,而是在它们中间产生了一个临时变量(double类型、常量),并利用这个临时变量赋值 //也就是说const double& j = h;就意味着j不是直接变成h的别名,而是变成临时变量(doublde类型)的别名,但是这个临时变量是一个常量,这也解释了为啥需要加上const }
💨小结
1️⃣ 我能否满足你变成别名的条件:可以不变或者缩小你读写的权限 (const int -> const int 或 int -> const int),而不能放大你读写的权限 (const int -> int)
2️⃣ 别名的意义可以改变,并不是每个别名都跟原名有一样的权限
3️⃣ 不能给类型不同的变量起别名的真正原因不是类型不同,而是隐式类型转换后具有常性了
❗ 常引用的意义 (举例栈) ❕
typedef struct Stack { int* a; int top; int capacity; }ST; void InitStack(ST& s)//传引用是为了形参的改变影响实参 {//...} void PrintStack(const ST& s)//1.传引用是为了减少拷贝 2. 同时保护实参不会被修改 {//...} void Test(const int& n)//即可以接收变量,也可以接收常量 {//...} int main() { ST st; InitStack(st); //... PrintStack(st); int i = 10; Test(i); Test(20); return 0; }
💨小结
1️⃣ 函数传参如果想减少拷贝使用引用传参,如果函数中不改变这个参数最好使用 const 引用传参
2️⃣ const 引用的好处是保护实参,避免被误改,且它可以传普通对象也可以传 const 对象
💦 使用场景
❗ 1、做参数 ❕
void Swap1(int* p1, int* p2) { int temp = *p1; *p1 = *p2; *p2 = temp; } void Swap2(int& rx, int& ry) { int temp = rx; rx = ry; ry = temp; } int main() { int x = 3, y = 5; Swap1(&x, &y);//C传参 Swap2(x, y);//C++传参 return 0; }
💨在 C++ 中形参变量的改变,要影响实参,可以用指针或者引用解决
意义:指针实现单链表尾插 || 引用实现单链表尾插
指针
引用
void SListPushBack(SLTNode*& phead, int x) { //这里phead的改变就是plist的改变 } void TestSList2() { SLTNode* plist = NULL; SListPushBack(plist, 1); SListPushBack(plist, 2); }
有些书上喜欢这样写 (不推荐)
typedef int SLTDataType; typedef struct SListNode { SLTDataType data; struct SListNode* next; }SLTNode, *PSLTNode; void SListPushBack(PSLTNode& phead, int x) { //... }
❗ 2、做返回值 ❕
2.1、传值返回
//传值返回 int Add(int a, int b) { int c = a + b; return c;//需要拷贝 } int main() { int ret = Add(1, 2);//ok, 3 Add(3, 4); cout << "Add(1, 2) is :"<< ret <<endl; return 0; }
💨Add 函数里的 return c; —— 传值返回,临时变量作返回值。如果比较小,通常是寄存器;如果比较大,会在 main 函数里开辟一块临时空间
怎么证明呢
int Add(int a, int b) { int c = a + b; return c; } int main() { //int& ret = Add(1, 2);//err const int& ret = Add(1, 2);//ok, 3 Add(3, 4); cout << "Add(1, 2) is :"<< ret <<endl; return 0; }
💨从上面就可以验证 Add 函数的返回值是先存储在临时空间里的
2.2、传引用返回
//传引用返回 int& Add(int a, int b) { int c = a + b; return c;//不需要拷贝 } int main() { int ret = Add(1, 2);//err, 3 Add(3, 4); cout << "Add(1, 2) is :"<< ret <<endl; return 0; }
💨结果是不确定的,因为 Add 函数的返回值是 c 的别名,所以在赋给 ret 前,c 的值到底是 3 还是随机值,跟平台有关系 (具体是平台销毁栈帧时是否会清理栈帧空间),所以这里的这种写法本身就是越界的 (越界抽查不一定报错)、错误的
发现这样也能跑,但诡异的是为啥 ret 是 7
//传引用返回 int& Add(int a, int b) { int c = a + b; return c; } int main() { int& ret = Add(1, 2);//err, 7 Add(3, 4); cout << "Add(1, 2) is :"<< ret <<endl; return 0; }
💨在上面我们在 VS 下运行,可以得出编译器并没有清理栈帧,那么这里进一步验证引用返回的危害
虽然能正常运行,但是它是有问题的
小结引用做返回值
1️⃣ 出了 TEST 函数的作用域,ret 变量会销毁,就不能引用返回
2️⃣ 出了 TEST 函数的作用域,ret 变量不会销毁,就可以引用返回
3️⃣ 引用返回的价值是减少拷贝
❓ 观察并剖析以下代码 ❔
int main() { int x = 3, y = 5; int* p1 = &x; int* p2 = &y; int*& p3 = p1; *p3 = 10; p3 = p2; return 0; }
💦 函数参数及返回值 ———— 传值、传引用效率比较
#include <time.h> #include<iostream> using namespace std; struct A { int a[10000]; }; A a; void TestFunc1(A a) {} void TestFunc2(A& a) {} A TestFunc3() { return a; } A& TestFunc4() { return a; } void TestRefAndValue() { 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; } void TestReturnByRefOrValue() { // 以值作为函数的返回值类型 size_t begin1 = clock(); for (size_t i = 0; i < 100000; ++i) TestFunc3(); size_t end1 = clock(); // 以引用作为函数的返回值类型 size_t begin2 = clock(); for (size_t i = 0; i < 100000; ++i) TestFunc4(); size_t end2 = clock(); // 计算两个函数运算完成之后的时间 cout << "TestFunc1 time:" << end1 - begin1 << endl; cout << "TestFunc2 time:" << end2 - begin2 << endl; } int main() { //传值、传引用效率比较 TestRefAndValue(); cout << "----------cut----------" << endl; //值和引用作为返回值类型的性能比较 TestReturnByRefOrValue(); return 0; }
💨以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低
💦 引用和指针的区别
1️⃣ 语法概念 1️⃣
引用就是一个别名,没有独立空间,和其引用实体共用同一块空间
指针变量是开辟一块空间,存储变量的地址
int main() { int a = 10; int& ra = a; cout<<"&a = "<<&a<<endl; cout<<"&ra = "<<&ra<<endl; int b = 20; int* pb = &b; cout<<"&b = "<<&b<<endl; cout<<"&pb = "<<&pb<<endl; return 0; }
2️⃣ 底层实现 2️⃣
引用和指针是一样的,因为引用是按照指针方式来实现的
int main() { int a = 10; int& ra = a; ra = 20; int* pa = &a; *pa = 20; return 0; }
这里我们对比一下 VS 下引用和指针的汇编代码可以看出来他俩是同根同源
引用和指针的不同点:
1、引用在定义时必须初始化,指针没有要求
2、引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
3 、没有 NULL 引用,但有 NULL 指针
4、在 sizeof 中含义不同:引用结果为引用类型的大小,与类型有关;但指针始终是地址空间所占字节个数 (32 位平台下占 4 个字节,64 位平台下占 8 个字节),与类型无关
5、引用自加即引用的实体增加 1,与类型无关,指针自加即指针向后偏移一个类型的大小,与类型有关
6、有多级指针,但是没有多级引用
7、访问实体方式不同,指针需要解引用,引用编译器自己处理
8、引用比指针使用起来相对更安全,指针容易出现野指针、空指针等非法访问问题
七、内联函数
💦 什么是内联函数
在程序中大量重复的建立函数栈帧 (如 swap 等函数) 会造成很大的性能开销,当然 C 语言可以用宏来代替函数,使之不会开辟栈帧,但是宏优点多,但也有不少的劣势,这时内联函数就可以针对这种场景解决问题 (内联函数对标宏函数)。
以 inline 修饰的函数叫做内联函数,编译时 C++ 编译器会在调用内联函数的地方展开,没有函数调用建立压栈的开销,内联函数提升程序运行的效率。
#include<iostream> using namespace std; //Add就会在调用的地方展开 inline int Add(int x, int y) { return x + y; } int main() { int ret = Add(10, 20); cout << ret << endl; return 0; }
❓ 验证 ❔
1、在 release 模式下,查看编译器生成的汇编是否存在 call Add
2、在 debug 模式下,需要对编译器设置,否则不会展开 (因为 debug 下,编译器默认不会对代码进行优化,以下是 VS2017 的设置方式)
💦 内联函数的特性
1️⃣ inline 是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。
2️⃣ inline 对于编译器而言只是一个建议,编译器会自动优化,如果定义为 inline 的函数体内有循环/递归等等,编译器优化时会忽略掉内联。
3️⃣ inline 不建议声明和定义分离,分离会导致链接错误。因为 inline 被展开,就没有函数地址了,链接就会找不到。
// F.h #include <iostream> using namespace std; inline void f(int i); // F.cpp #include "F.h" void f(int i) { cout << i << endl; } // main.cpp #include "F.h" int main() { f(10); return 0; } // 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用
八、auto关键字 (C++11)
注意 C++11 一般要标准之后的编译器才支持的比较好 (最少 2013)
💦 auto简介
在早期 C/C++ 中 auto 的含义是:使用 auto 修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它,大家可以思考下为啥?
C++11 中,标准委员会赋予了 auto 全新的含义即:auto 不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto 声明的变量必须由编译器在编译时期推导而得。
int main() { int a = 3; char b = 'A'; //通过右边的赋值对象,自动推导变量类型 auto c = a; auto d = b; //typeid可以去看变量的实际类型 cout << typeid(c).name() << endl; cout << typeid(d).name() << endl; return 0; }
⚠ 注意
使用 auto 定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导 auto 的实际类型。因此 auto 并非是一种 “类型” 的声明,而是一个类型声明时的 “占位符”,编译器在编译期会将 auto 替换为变量实际的类型。
❓ auto的价值 ❔
auto 在前期并不能很好的体现它的价值,在后面学了 STL 时就能体现了,这里我们可以先看看。
map<string, string> dict; //map<string, string> :: iterator it = dict.begin(); auto it = dict.begin;//同上
从上就可以看出 auto 的价值就是简化代码
💨优点:auto 可以自动推导类型简化代码
💨缺点:一定程度上牺牲了代码的可读性
💦 auto的使用细则
1️⃣ auto与指针和引用结合起来使用 1️⃣
用 auto 声明指针类型时,用 auto 和 auto* 没有任何区别;但用 auto 声明引用类型时则必须加 &
int main() { int x = 10; auto a = &x; auto* b = &x; auto& c = x; cout << typeid(a).name() << endl;//int* cout << typeid(b).name() << endl;//int* cout << typeid(c).name() << endl;//int *a = 20; *b = 30; c = 40; return 0; }
2️⃣ 在同一行定义多个变量 2️⃣
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
void TestAuto() { auto a = 1, b = 2; //ok auto c = 3, d = 4.0; //该行代码会编译失败,因为c和d的初始化表达式类型不同 }
💦 auto不能推导的场景
1️⃣ auto 不能作为函数的参数 1️⃣
//此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导 void TestAuto(auto a) {}
2️⃣ auto 不能直接用来声明数组 2️⃣
void TestAuto() { int a[] = {1,2,3}; auto b[] = {4,5,6}; }
3️⃣ 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法 3️⃣
❗ C++98 auto 和 C++11 auto❕
早期 C++98 标准中就存在了 auto 关键字,那时的 auto 用于声明变量为自动变量,拥有自动的生命周期;但是这个作用是多余的,因为变量默认拥有自动的生命周期
在 C++11 中,已经删除了这种用法,取而代之的用处是自动推断变量的类型
4️⃣ auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用 4️⃣
九、基于范围的for循环 (C++11)
💦 范围for的语法
在 C++98 中如果要遍历一个数组,可以按照以下方式进行:
void TestFor() { int array[] = { 1, 2, 3, 4, 5 }; for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i) array[i] *= 2; for (int* p = array; p < array + sizeof(array)/ sizeof(array[0]); ++p) cout << *p << endl; }
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此 C++11 中引入了基于范围的 for 循环。for 循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
void TestFor() { int array[] = { 1, 2, 3, 4, 5 }; //自动依次取数组中的值给e并自动判断结束 for(auto e : array) cout << e << " "; //这里要对数组里的内容进行改变,就要使用&,因为这里的e只是一份拷贝 for(auto& e : array) e *= 2; return 0; }
⚠ 注意:
范围 for 与普通循环类似,可以用 continue 来结束本次循环,也可以用 break 来跳出整个循环。这个东西后面到了容器会详细介绍
💦 范围for的使用条件
1️⃣ for循环迭代的范围必须是确定的 1️⃣
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供 begin 和 end 的方法,begin 和 end 就是 for 循环迭代的范围。
以下代码就有问题,因为 for 的范围不确定
void TestFor(int array[]) { //注意这里的array已经不是数组了,已经退化为指针了 for(auto& e : array) cout<< e <<endl; }
2️⃣ 迭代的对象要实现++和==的操作 2️⃣
关于迭代器这个问题,以后会讲,在这篇文章只做为了解
十、指针空值 —— nullptr (C++11)
💦 C++98中的指针空值
在良好的 C/C++ 编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化
void TestPtr() { //C++98 int* p1 = NULL; int* p2 = 0; //C++11 int* p3 = nullptr; // …… }
⚠ 注意:
指针本质是内存按字节为单位的编号,空指针并不是不存在的指针,而是第一个字节的编号,一般我们不使用这个空间存有效数据。空指针一般用来初始化,表示指针没有指向一块有效数据的空间。
❓ 为啥C++11后推荐用nullptr来初始化空指针 ❔
NULL 实际是一个宏,在传统的 C 头文件 (stddef.h) 中,可以看到如下代码:
场景:这里本意上是想让 0 匹配 int、NULL 匹配 int*
void f(int) { cout<<"f(int)"<<endl; } void f(int*) { cout<<"f(int*)"<<endl; } int main() { f(0); f(NULL); //f((int*)NULL); //f(nullptr);//使用C++11中的nullptr更准确 return 0; }
由于在 C++98 中 NULL 被定义成 0,因此与程序的初衷相悖
在 C++98 中,字面常量 0 既可以是一个整形数字,也可以是无类型的指针 (void*) 常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转 (void *)0。
⚠ 注意:
1️⃣ 在使用 nullptr 表示指针空值时,不需要包含头文件,因为 nullptr 是 C++11 作为新关键字引入的。
2️⃣在C++11中,sizeof(nullptr) 与 sizeof((void*)0) 所占的字节数相同。
3️⃣为了提高代码的健壮性,在后续表示指针空值时建议最好使用 nullptr。