C++完全攻略:从新手到高手的编程进化之路
文章目录
- C++完全攻略:从新手到高手的编程进化之路
C++完全攻略:从新手到高手的编程进化之路
1.C++发展历史
C++的起源可以追溯到1979年,当时BjarneStroustrup(本贾尼·斯特劳斯特卢普,这个翻译的名字不 同的地⽅可能有差异)在⻉尔实验室从事计算机科学和软件⼯程的研究⼯作。⾯对项⽬中复杂的软件开 发任务,特别是模拟和操作系统的开发⼯作,他感受到了现有语⾔(如C语⾔)在表达能⼒、可维护性 和可扩展性⽅⾯的不⾜。 1983年,BjarneStroustrup在C语⾔的基础上添加了⾯向对象编程的特性,设计出了C++语⾔的雏形, 此时的C++已经有了类、封装、继承等核⼼概念,为后来的⾯向对象编程奠定了基础。这⼀年该语⾔被 正式命名为C++。 在随后的⼏年中,C++在学术界和⼯业界的应⽤逐渐增多。⼀些⼤学和研究所开始将C++作为教学和研 究的⾸选语⾔,⽽⼀些公司也开始在产品开发中尝试使⽤C++。这⼀时期,C++的标准库和模板等特性 也得到了进⼀步的完善和发展。 C++的标准化⼯作于1989年开始,并成⽴了⼀个ANSI和ISO(InternationalStandards Organization)国际标准化组织的联合标准化委员会。1994年标准化委员会提出了第⼀个标准化草 案。在该草案中,委员会在保持斯特劳斯特卢普最初定义的所有特征的同时,还增加了部分新特征。 在完成C++标准化的第⼀个草案后不久,STL(StandardTemplateLibrary)是惠普实验室开发的⼀系 列软件的统称。它是由AlexanderStepanov、MengLee和DavidRMusser在惠普实验室⼯作时所开发 出来的。在通过了标准化第⼀个草案之后,联合标准化委员会投票并通过了将STL包含到C++标准中的 提议。STL对C++的扩展超出C++的最初定义范围。虽然在标准中增加STL是个很重要的决定,但也因 此延缓了C++标准化的进程。 1997年11⽉14⽇,联合标准化委员会通过了该标准的最终草案。1998年,C++的ANSI/IS0标准被投⼊ 使⽤。
2. C++版本更新
if constexpr
、折叠表达式,改进string
、filesystem
等if consteval
、flat_map
、import std
等3.C++参考文档
https://legacy.cplusplus.com/reference/
https://zh.cppreference.com/w/cpp
https://en.cppreference.com/w/
说明:第⼀个链接不是C++官⽅⽂档,标准也只更新到C++11,但是以头⽂件形式呈现,内容⽐较易看 好懂。后两个链接分别是C++官⽅⽂档的中⽂版和英⽂版,信息很全,更新到了最新的C++标准,但是 相⽐第⼀个不那么易看;⼏个⽂档各有优势,我们结合着使⽤。
4.C++的重要性
4.1 编程语言排行榜
TIOBE排⾏榜是根据互联⽹上有经验的程序员、课程和第三⽅⼚商的数量,并使⽤搜索引擎(如
Google、Bing、Yahoo!)以及Wikipedia、Amazon、YouTube和Baidu(百度)统计出排名数据,只是反映某个编程语⾔的热⻔程度,并不能说明⼀⻔编程语⾔好不好,或者⼀⻔语⾔所编写的代码数量多少。
4.2 C++在⼯作领域中的应用
C++的应⽤领域服务器端、游戏(引擎)、机器学习引擎、⾳视频处理、嵌⼊式软件、电信设备、⾦融 应⽤、基础库、操作系统、编译器、基础架构、基础⼯具、硬件交互等很多⽅⾯都有。
1.⼤型系统软件开发。如编译器、数据库、操作系统、浏览器等等
2.⾳视频处理。常⻅的⾳视频开源库和⽅案有FFmpeg、WebRTC、Mediasoup、ijkplayer,⾳视频 开发最主要的技术栈就是C++。
3.PC客⼾端开发。⼀般是开发Windows上的桌⾯软件,⽐如WPS之类的,技术栈的话⼀般是C++和 QT,QT是⼀个跨平台的C++图形⽤⼾界⾯(GraphicalUserInterface,GUI)程序。
4.服务端开发。各种⼤型应⽤⽹络连接的⾼并发后台服务。这块Java也⽐较多,C++主要⽤于⼀些对 性能要求⽐较⾼的地⽅。如:游戏服务、流媒体服务、量化⾼频交易服务等
5.游戏引擎开发。很多游戏引擎就都是使⽤C++开发的,游戏开发要掌握C++基础和数据结构,学习 图形学知识,掌握游戏引擎和框架,了解引擎实现,引擎源代码可以学习UE4、Cocos2d-x等开源 引擎实现
6.嵌⼊式开发。嵌⼊式把具有计算能⼒的主控板嵌⼊到机器装置或者电⼦装置的内部,通过软件能够 控制这些装置。⽐如:智能⼿环、摄像头、扫地机器⼈、智能⾳响、⻔禁系统、⻋载系统等等,粗 略⼀点,嵌⼊式开发主要分为嵌⼊式应⽤和嵌⼊式驱动开发.
7.机器学习引擎。机器学习底层的很多算法都是⽤C++实现的,上层⽤python封装起来。如果你只想 准备数据训练模型,那么学会Python基本上就够了,如果你想做机器学习系统的开发,那么需要学 会C++。
8.测试开发/测试。每个公司研发团队,有研发就有测试,测试主要分为测试开发和功能测试,测试 开发⼀般是使⽤⼀些测试⼯具(selenium、Jmeter等),设计测试⽤例,然后写⼀些脚本进⾏⾃动化 测试,性能测试等,有些还需要⾃⾏开发⼀些测试⽤具。功能测试主要是根据产品的功能,设计测 试⽤例,然后⼿动的⽅式进⾏测试。
5.C++的第⼀个程序
C++兼容C语⾔绝⼤多数的语法,所以C语⾔实现的helloworld依旧可以运⾏,C++中需要把定义⽂件 代码后缀改为.cpp,vs编译器看到是.cpp就会调⽤C++编译器编译,linux下要⽤**g++**编译,不再是gcc
// test.cpp#includeint main(){ printf(\"hello world\\n\"); return 0;}
当然C++有⼀套⾃⼰的输⼊输出,严格说C++版本的helloworld应该是这样写的。
// test.cpp// 这⾥的std cout等我们都看不懂,没关系,下⾯我们会依次讲解 #includeusing namespace std;int main(){ cout << \"hello world\\n\" << endl;//这里的endline相当于换行的作用 return 0;}
6.命名空间
6.1namespace的价值
在C/C++中,变量、函数和后⾯要学到的类都是⼤量存在的,这些变量、函数和类的名称将都存在于全局作⽤域中,可能会导致很多冲突。使⽤命名空间的⽬的是对标识符的名称进⾏本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
c语⾔项⽬类似下⾯程序这样的命名冲突是普遍存在的问题,C++引⼊namespace就是为了更好的解决这样的问题
#include #include int rand = 10;int main(){ // 编译报错:error C2365: “rand”: 重定义;以前的定义是“函数” printf(\"%d\\n\", rand); return 0;}
6.2 namespace的定义
• 定义命名空间,需要使⽤到namespace关键字,后⾯跟命名空间的名字,然后接⼀对{}即可,{}中即为命名空间的成员。命名空间中可以定义变量/函数/类型等。
• namespace本质是定义出⼀个域,这个域跟全局域各⾃独⽴,不同的域可以定义同名变量,所以下⾯的rand不在冲突了。
• C++中域有函数局部域,全局域,命名空间域,类域;域影响的是编译时语法查找⼀个变量/函数/类型出处(声明或定义)的逻辑,所有有了域隔离,名字冲突就解决了。局部域和全局域除了会影响编译查找逻辑,还会影响变量的⽣命周期,命名空间域和类域不影响变量⽣命周期。
• namespace只能定义在全局,当然他还可以嵌套定义。
• 项⽬⼯程中多⽂件中定义的同名namespace会认为是⼀个namespace,不会冲突。
• C++标准库都放在⼀个叫std(standard)的命名空间中。
#include #include // 1. 正常的命名空间定义 // fnx是命名空间的名字,⼀般开发中是⽤项⽬名字做命名空间名。 namespace fnx{ // 命名空间中可以定义变量/函数/类型 int rand = 10; int Add(int left, int right) { return left + right; } struct Node { struct Node* next; int val; };}int main(){ // 这⾥默认是访问的是全局的rand函数指针 printf(\"%p\\n\", rand); // 这⾥指定bit命名空间中的rand printf(\"%d\\n\", fnx::rand); return 0;}
//2. 命名空间可以嵌套 namespace fnx{ // czx的代码 namespace czx { int rand = 1; int Add(int left, int right) { return left + right; } } // lgh的代码 namespace lgh { int rand = 2; int Add(int left, int right) { return (left + right)*10; } }}int main(){ printf(\"%d\\n\", bit::pg::rand); printf(\"%d\\n\", bit::hg::rand); printf(\"%d\\n\", bit::pg::Add(1, 2)); printf(\"%d\\n\", bit::hg::Add(1, 2)); return 0;}
多⽂件中可以定义同名namespace,他们会默认合并到⼀起,就像同⼀个namespace⼀样
// Stack.h#pragma once#include#include#include#includenamespace fnx{ typedef int STDataType; typedef struct Stack { STDataType* a; int top; int capacity; }ST; void STInit(ST* ps, int n); void STDestroy(ST* ps); void STPush(ST* ps, STDataType x); void STPop(ST* ps); STDataType STTop(ST* ps); int STSize(ST* ps); bool STEmpty(ST* ps);}// Stack.cpp#include\"Stack.h\"namespace fnx{ void STInit(ST* ps, int n) { assert(ps); ps->a = (STDataType*)malloc(n * sizeof(STDataType)); ps->top = 0; ps->capacity = n; } // 栈顶 void STPush(ST* ps, STDataType x) { assert(ps); // 满了, 扩容 if (ps->top == ps->capacity) { printf(\"扩容\\n\"); int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2; STDataType* tmp = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType)); if (tmp == NULL) { perror(\"realloc fail\"); return; } ps->a = tmp; ps->capacity = newcapacity; } ps->a[ps->top] = x; ps->top++; } //...}// Queue.h#pragma once#include#include#includenamespace fnx{ typedef int QDataType; typedef struct QueueNode { int val; struct QueueNode* next; }QNode; typedef struct Queue { QNode* phead; QNode* ptail; int size; }Queue; void QueueInit(Queue* pq); void QueueDestroy(Queue* pq); // ⼊队列 void QueuePush(Queue* pq, QDataType x); // 出队列 void QueuePop(Queue* pq); QDataType QueueFront(Queue* pq); QDataType QueueBack(Queue* pq); bool QueueEmpty(Queue* pq); int QueueSize(Queue* pq);}// Queue.cpp#include\"Queue.h\"namespace fnx{ void QueueInit(Queue* pq) { assert(pq); pq->phead = NULL; pq->ptail = NULL; pq->size = 0; } // ...}// test.cpp#include\"Queue.h\"#include\"Stack.h\"// 全局定义了⼀份单独的Stack typedef struct Stack{ int a[10]; int top;}ST;void STInit(ST* ps){}void STPush(ST* ps, int x){}int main(){ // 调⽤全局的 ST st1; STInit(&st1); STPush(&st1, 1); STPush(&st1, 2); printf(\"%d\\n\", sizeof(st1)); // 调⽤fnx namespace的 fnx::ST st2; printf(\"%d\\n\", sizeof(st2)); bit::STInit(&st2); bit::STPush(&st2, 1); bit::STPush(&st2, 2); return 0;
6.3 命名空间使用
编译查找⼀个变量的声明/定义时,默认只会在局部或者全局查找,不会到命名空间⾥⾯去查找。所以下⾯程序会编译报错。所以我们要使⽤命名空间中定义的变量/函数,有三种⽅式:
• 指定命名空间访问,项⽬中推荐这种⽅式。
• using将命名空间中某个成员展开,项⽬中经常访问的不存在冲突的成员推荐这种⽅式。
• 展开命名空间中全部成员,项⽬不推荐,冲突⻛险很⼤,⽇常⼩练习程序为了⽅便推荐使⽤。
#includenamespace N{ int a = 0; int b = 1;}int main(){ // 编译报错:error C2065: “a”: 未声明的标识符 printf(\"%d\\n\", a); return 0;}// 指定命名空间访问 int main(){ printf(\"%d\\n\", N::a); return 0; }// using将命名空间中某个成员展开 using N::b;int main(){ printf(\"%d\\n\", N::a); printf(\"%d\\n\", b); return 0; }// 展开命名空间中全部成员 using namespce N;int main(){ printf(\"%d\\n\", a); printf(\"%d\\n\", b); return 0; }
7.C++输⼊&输出
• iostream是InputOutputStream的缩写,是标准的输⼊、输出流库,定义了标准的输⼊、输出对象。
• std::cin是istream类的对象,它主要⾯向窄字符(narrow characters of type char))的标准输
⼊流。
• std::cout是ostream类的对象,它主要⾯向窄字符的标准输出流。
• std::endl是⼀个函数,流插⼊输出时,相当于插⼊⼀个换行字符加刷新缓冲区。
• <>是流提取运算符。(C语⾔还⽤这两个运算符做位运算左移/右移)
• 使⽤C++输⼊输出更方便,不需要像printf/scanf输⼊输出时那样,需要⼿动指定格式,C++的输⼊输出可以⾃动识别变量类型(本质是通过函数重载实现的),其实最重要的是
C++的流能更好的⽀持⾃定义类型对象的输⼊输出。
• IO流涉及类和对象,运算符重载、继承等很多⾯向对象的知识,这些知识我们还没有讲解,所以这⾥我们只能简单认识⼀下C++ IO流的⽤法,后⾯我们会有专⻔的⼀个章节来细节IO流库。
• cout/cin/endl等都属于C++标准库,C++标准库都放在⼀个叫std(standard)的命名空间中,所以要通过命名空间的使⽤⽅式去⽤他们。
• ⼀般⽇常练习中我们可以using namespacestd,实际项⽬开发中不建议using namespace std。
• 这⾥我们没有包含,也可以使⽤printf和scanf,在包含iostream间接包含了。vs系列编译器是这样的,其他编译器可能会报错。
#define _CRT_SECURE_NO_WARNINGS 1#include using namespace std;int main(){ int a = 0; double b = 0.1; char c = \'x\'; cout << a << \" \" << b << \" \" << c << endl; std::cout << a << \" \" << b << \" \" << c <> a; cin >> b >> c; cout << a << endl; cout << b << \" \" << c << endl; return 0;}#includeusing namespace std;int main(){ // 在io需求⽐较⾼的地⽅,如部分⼤量输⼊的竞赛题中,加上以下3⾏代码 // 可以提⾼C++IO效率 ios_base::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); return 0;}
8.缺省参数
• 缺省参数是声明或定义函数时为函数的参数指定⼀个缺省值。在调⽤该函数时,如果没有指定实参则采⽤该形参的缺省值,否则使用指定的实参,缺省参数分为全缺省和半缺省参数。(有些地⽅把缺省参数也叫默认参数)
• 全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。C++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。
• 带缺省参数的函数调⽤**,C++规定必须从左到右依次给实参,不能跳跃给实参。**
• 函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省
值。
#include #include using namespace std;void Func(int a = 0){ cout << a << endl;}int main(){ Func(); // 没有传参时,使⽤参数的默认值 Func(10); // 传参时,使⽤指定的实参 return 0;}
#include using namespace std;// 全缺省 void Func1(int a = 10, int b = 20, int c = 30){ cout << \"a = \" << a << endl; cout << \"b = \" << b << endl; cout << \"c = \" << c << endl << endl;}// 半缺省 void Func2(int a, int b = 10, int c = 20){ cout << \"a = \" << a << endl; cout << \"b = \" << b << endl; cout << \"c = \" << c << endl << endl;}int main(){ Func1(); Func1(1); Func1(1,2); Func1(1,2,3); Func2(100); Func2(100, 200); Func2(100, 200, 300); return 0;}
// Stack.h#include #include using namespace std;typedef int STDataType;typedef struct Stack{ STDataType* a; int top; int capacity;}ST;void STInit(ST* ps, int n = 4);// Stack.cpp#include\"Stack.h\"// 缺省参数不能声明和定义同时给 void STInit(ST* ps, int n){ assert(ps && n > 0); ps->a = (STDataType*)malloc(n * sizeof(STDataType)); ps->top = 0; ps->capacity = n;}// test.cpp#include\"Stack.h\"int main(){ ST s1; STInit(&s1); // 确定知道要插⼊1000个数据,初始化时⼀把开好,避免扩容 ST s2; STInit(&s2, 1000); return 0;}
9.函数重载
C++⽀持在同⼀作⽤域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者 类型不同。这样C++函数调⽤就表现出了多态⾏为,使⽤更灵活。C语⾔是不⽀持同⼀作⽤域中出现同 名函数的。
#includeusing namespace std;// 1、参数类型不同 int Add(int left, int right){ cout << \"int Add(int left, int right)\" << endl; return left + right;}double Add(double left, double right){ cout << \"double Add(double left, double right)\" << endl; return left + right;}// 2、参数个数不同 void f(){ cout << \"f()\" << endl;}void f(int a){ cout << \"f(int a)\" << endl;}// 3、参数类型顺序不同 void f(int a, char b){ cout << \"f(int a,char b)\" << endl;}void f(char b, int a){ cout << \"f(char b, int a)\" << endl;} // 返回值不同不能作为重载条件,因为调⽤时也⽆法区分 //void fxx() //{} // //int fxx() //{ // return 0; //} // 下⾯两个函数构成重载 // f()但是调⽤时,会报错,存在歧义,编译器不知道调⽤谁 void f1() { cout << \"f()\" << endl; } void f1(int a = 10) { cout << \"f(int a)\" << endl; } int main() { Add(10, 20); Add(10.1, 20.2); f(); f(10); f(10, \'a\'); f(\'a\', 10); return 0; }
10引用
10.1 引用的概念和定义
引⽤不是新定义⼀个变量,⽽是给已存在变量取了⼀个别名,编译器不会为引⽤变量开辟内存空间,它和它引⽤的变量共⽤同⼀块内存空间。⽐如:⽔壶传中李逵,宋江叫\"铁⽜\",江湖上⼈称\"⿊旋
⻛\";林冲,外号豹⼦头;
类型&引⽤别名=引⽤对象;C++中为了避免引⼊太多的运算符,会复⽤C语⾔的⼀些符号,⽐如前⾯的<>,这⾥引⽤也和取地址使⽤了同⼀个符号&,⼤家注意使⽤⽅法⻆度区分就可以。(吐槽⼀下,这个问题其实挺坑的,个⼈觉得⽤更多符号反⽽更好,不容易混淆)
#includeusing namespace std;int main(){ int a = 0; // 引⽤:b和c是a的别名 int& b = a; int& c = a; // 也可以给别名b取别名,d相当于还是a的别名 int& d = b; ++d; // 这⾥取地址我们看到是⼀样的 cout << &a << endl; cout << &b << endl; cout << &c << endl; cout << &d << endl; return 0;}
10.2 引用的特性
• 引⽤在定义时必须初始化
• ⼀个变量可以有多个引⽤
• 引⽤⼀旦引⽤⼀个实体,再不能引⽤其他实体
#includeusing namespace std;int main(){ int a = 10; // 编译报错:“ra”: 必须初始化引⽤ //int& ra; int& b = a; int c = 20; // 这⾥并⾮让b引⽤c,因为C++引⽤不能改变指向, // 这⾥是⼀个赋值 b = c; cout << &a << endl; cout << &b << endl; cout << &c << endl; return 0;}
10.3 引用的使用
• 引⽤在实践中主要是于引⽤传参和引⽤做返回值中减少拷⻉提⾼效率和改变引⽤对象时同时改变被引⽤对象。
• 引⽤传参跟指针传参功能是类似的,引⽤传参相对更⽅便⼀些。
• 引⽤返回值的场景相对⽐较复杂,我们在这⾥简单讲了⼀下场景,还有⼀些内容后续类和对象章节中会继续深⼊讲解。
• 引⽤和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C++的引⽤跟其他语⾔的引⽤(如Java)是有很⼤的区别的,除了⽤法,最⼤的点,C++引⽤定义后不能改变指向,
Java的引⽤可以改变指向。
• ⼀些主要⽤C代码实现版本数据结构教材中,使⽤C++引⽤替代指针传参,⽬的是简化程序,避开复杂的指针,但是很多同学没学过引⽤,导致⼀头雾⽔。
void Swap(int& rx, int& ry){ int tmp = rx; rx = ry; ry = tmp;}int main(){ int x = 0, y = 1; cout << x <<\" \" << y << endl; Swap(x, y); cout << x << \" \" << y << endl; return 0;}
#includeusing namespace std;typedef int STDataType;typedef struct Stack{ STDataType* a; int top; int capacity;}ST;void STInit(ST& rs, int n = 4){ rs.a = (STDataType*)malloc(n * sizeof(STDataType)); rs.top = 0; rs.capacity = n;}// 栈顶 void STPush(ST& rs, STDataType x){ assert(ps); // 满了, 扩容 if (rs.top == rs.capacity) { printf(\"扩容\\n\"); int newcapacity = rs.capacity == 0 ? 4 : rs.capacity * 2; STDataType* tmp = (STDataType*)realloc(rs.a, newcapacity * sizeof(STDataType)); if (tmp == NULL) { perror(\"realloc fail\"); return; } rs.a = tmp; rs.capacity = newcapacity; } rs.a[rs.top] = x; rs.top++;}// int STTop(ST& rs)int& STTop(ST& rs){ assert(rs.top > 0); return rs.a[rs.top];}int main(){ // 调⽤全局的 ST st1; STInit(st1); STPush(st1, 1); STPush(st1, 2); cout << STTop(st1) << endl; STTop(st1) += 10; cout << STTop(st1) << endl; return 0;}
#includeusing namespace std;typedef struct SeqList{ int a[10]; int size;}SLT;// ⼀些主要⽤C代码实现版本数据结构教材中,使⽤C++引⽤替代指针传参,⽬的是简化程序,避开复杂的指针,但是很多同学没学过引⽤,导致⼀头雾⽔。 void SeqPushBack(SLT& sl, int x){}typedef struct ListNode{ int val; struct ListNode* next;}LTNode, *PNode;// 指针变量也可以取别名,这⾥LTNode*& phead就是给指针变量取别名 // 这样就不需要⽤⼆级指针了,相对⽽⾔简化了程序 //void ListPushBack(LTNode** phead, int x)//void ListPushBack(LTNode*& phead, int x)void ListPushBack(PNode& phead, int x){ PNode newnode = (PNode)malloc(sizeof(LTNode)); newnode->val = x; newnode->next = NULL; if (phead == NULL) { phead = newnode; } else { //... }}int main(){ PNode plist = NULL; ListPushBack(plist, 1); return 0;}
10.4 const引用
• 可以引⽤⼀个const对象,但是必须⽤const引⽤。const引⽤也可以引⽤普通对象,因为对象的访问权限在引⽤过程中可以缩⼩,但是不能放⼤。
• 不需要注意的是类似 int& rb = a3; double d = 12.34; int& rd = d; 这样⼀些场景下a3的和结果保存在⼀个临时对象中, int& rd = d 也是类似,在类型转换中会产⽣临时对象存储中间值,也就是时,rb和rd引⽤的都是临时对象,⽽C++规定临时对象具有常性,所以这⾥就触发了权限放⼤,必须要⽤常引⽤才可以。
• 所谓临时对象就是编译器需要⼀个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象,
C++中把这个未命名对象叫做临时对象。
int main(){ const int a = 10; // 编译报错:error C2440: “初始化”: ⽆法从“const int”转换为“int &” // 这⾥的引⽤是对a访问权限的放⼤ //int& ra = a; // 这样才可以 const int& ra = a; // 编译报错:error C3892: “ra”: 不能给常量赋值 //ra++; // 这⾥的引⽤是对b访问权限的缩⼩ int b = 20; const int& rb = b; // 编译报错:error C3892: “rb”: 不能给常量赋值 //rb++; return 0;}
#includeusing namespace std;int main(){ int a = 10; const int& ra = 30; // 编译报错: “初始化”: ⽆法从“int”转换为“int &” // int& rb = a * 3; const int& rb = a*3; double d = 12.34; // 编译报错:“初始化”: ⽆法从“double”转换为“int &” // int& rd = d; const int& rd = d; return 0;}
10.5 指针和引用的关系
C++中指针和引⽤就像两个性格迥异的亲兄弟,指针是哥哥,引⽤是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有⾃⼰的特点,互相不可替代。
• 语法概念上引用是⼀个变量的取别名不开空间,指针是存储⼀个变量地址,要开空间。
• 引⽤在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
• 引⽤在初始化时引⽤⼀个对象后,就不能再引用其他对象;而指针可以在不断地改变指向对象。
• 引⽤可以直接访问指向对象,指针需要解引用才是访问指向对象。
• sizeof中含义不同,引⽤结果为引⽤类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8byte)
• 指针很容易出现空指针和野指针的问题,引用很少出现,引用使用起来相对更安全⼀些。
11.inline
• ⽤inline修饰的函数叫做内联函数,编译时C++编译器会在调⽤的地⽅展开内联函数,这样调⽤内联函数就需要建⽴栈帧了,就可以提⾼效率。
• inline对于编译器⽽⾔只是⼀个建议,也就是说,你加了inline编译器也可以选择在调⽤的地⽅不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。inline适⽤于频繁调⽤的短⼩函数,对于递归函数,代码相对多⼀些的函数,加上inline也会被编译器忽略。
• C语⾔实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不⽅便调
试,C++设计了inline⽬的就是替代C的宏函数。
• vs编译器debug版本下⾯默认是不展开inline的,这样⽅便调试,debug版本想展开需要设置⼀下以下两个地⽅。
• inline不建议声明和定义分离到两个⽂件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错。
#includeusing namespace std;inline int Add(int x, int y){ int ret = x + y; ret += 1; ret += 1; ret += 1; return ret;}int main(){ // 可以通过汇编观察程序是否展开 // 有call Add语句就是没有展开,没有就是展开了 int ret = Add(1, 2); cout << Add(1, 2) * 5 << endl; return 0;}
#include using namespace std; // 实现⼀个ADD宏函数的常⻅问题 //#define ADD(int a, int b) return a + b; //#define ADD(a, b) a + b; //#define ADD(a, b) (a + b) // 正确的宏实现 #define ADD(a, b) ((a) + (b)) // 为什么不能加分号? // 为什么要加外⾯的括号? // 为什么要加⾥⾯的括号? int main() { int ret = ADD(1, 2); cout << ADD(1, 2) << endl; cout << ADD(1, 2)*5 < (x&y+x|y) return 0; }
// F.h#include 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(){ // 链接错误:⽆法解析的外部符号 \"void __cdecl f(int)\" (?f@@YAXH@Z) f(10); return 0;}
12.nullptr
NULL实际是⼀个宏,在传统的C头⽂件(stddef.h)中,可以看到如下代码:
#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif#endif
• C++中NULL可能被定义为字⾯常量0,或者C中被定义为⽆类型指针(void)的常量。不论采取何种定义,在使⽤空值的指针时,都不可避免的会遇到⼀些⿇烦,本想通过f(NULL)调⽤指针版本的
f(int * )函数,但是由于NULL被定义成0,调⽤了f(int x),因此与程序的初衷相悖。f((void)NULL);调⽤会报错。**
• C++11中引⼊nullptr,nullptr是⼀个特殊的关键字,nullptr是⼀种特殊类型的字⾯量,它可以转换成任意其他类型的指针类型。使⽤nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,⽽不能被转换为整数类型。
#includeusing namespace std;void f(int x){ cout << \"f(int x)\" << endl;}void f(int* ptr){ cout << \"f(int* ptr)\" << endl;}int main(){ f(0); // 本想通过f(NULL)调⽤指针版本的f(int*)函数,但是由于NULL被定义成0,调⽤了f(int x),因此与程序的初衷相悖。 f(NULL); f((int*)NULL); // 编译报错:error C2665: “f”: 2 个重载中没有⼀个可以转换所有参数类型 // f((void*)NULL); f(nullptr); return 0;}