1. C++发展历史
C++发展史:从创世纪到当下_比尔·古德斯特(bjarne stroustrup)-CSDN博客
版权声明:本文为博主原创文章
2. C++版本更新
版权声明:本文为博主原创文章
3. C++参考文档
注:非官方,仅到C++11,但以头文件形式呈现,内容易看懂。
注:C++官方中文版
注:C++官方英文版
4. C++的第一个程序
由C++历史我们可以得知:C++其实就是C的延续,或者说C++改良了一些C的漏洞并加入了自己的一些语法及用法。
在学习C语言的时候,我们所写的第一个程序应该都是用英文打印“你好,世界!”这句话。C++也不例外,不过C++有一套自己的输入输出方法,接下来就让我们一起来对比一下。
首先我们先来看一下C:
1. #include<stdio.h>
2. int main()
3. {
4. printf("hello world!");
5. return 0;
6. }
再来看一下C++:
1. #include<iostream>
2. using namespace std;
3. int main()
4. {
5. cout << "hello world!" << endl;
6. return 0;
7. }
我们先不用搞清楚为啥C++要这样写,以后会讲到。
对比我们可以发现C和C++有很多相同点,也有很多不同点。这也就验证了C++和C之间有着千丝万缕的联系。接下来我们先来看一下代码中的第二句代码using namespace std;这句代码中用到了namespace这个关键字,接下来我们看看这个关键字有什么用。
5.命名空间
在我们之前的C语言学习中,我们可能定义了一个变量跟某个头文件中的函数名重了,在没包含的时候,不会出现报错,但当我们需要使用这个头文件的时候,我们包含它,这个时候就会很烦,要去改制前的函数名。为了补足这个漏洞,我们的祖师爷本贾尼·斯特劳斯特卢普引用了命名空间这个概念。提到命名空间,又不得不提到域这个概念,接下来我们先讲讲什么是域。
5.1 域
C++中域有函数局部域,全局域,命名空间域,类域。当然,不划分这么细的话可以直接说就是作用域。
简单来说,一个{}就对应了一个作用域,作用域里的局部变量出了作用域就消亡了(根本原因是栈恢复)。而对于静态变量,编译器编译的时候做了处理,解析为“全局变量”,但这个全局变量和一般意义上的全局变量不同,它带有记号,这个记号就是 域,标志着这个所谓的全局变量是专属于这个域的。
域影响的是编译时语法查找一个变量/函数/类型出处(声明或定义)的逻辑。而有了域隔离,名字冲突就解决了。局部域和全局域除了会影响编译查找逻辑,还会影响变量的生命周期,命名空间域和类域不影响变量生命周期。
5.2 namespace的定义
- 定义命名空间,我们需要用到namespace这个关键字,后面跟命名空间的名子,然后接一对{}即可,{}中即为命名空间的成员。命名空间中可以定义变量/函数/类型等。
- namespace本质是定义一个域,这个域跟全局域各自独立,不同的域可以定义同名变量。
- namespace只能定义在全局,当然它还可以嵌套定义。
- 项目多文件中定义的同名的namespace会被编译器默认为是一个namespace,不会冲突。
- C++标准库都放在一个叫std(standard)的命名空间中。
下面是一些练习示例,主要解释一下上述的1,2,3,4点。
1. namespace sl
2. {
3. //定义变量
4.
5. int rand = 10;
6. //定义函数
7.
8. int ADD(int x, int y)
9. {
10. return x + y;
11. }
12. //定义结构体
13.
14. struct Node
15. {
16. struct Node* next;
17. int val;
18. };
19. }

这张图片可以看出并没有发生报错,由此也可以说明不同的命名空间中可以定义相同的变量名,这并不冲突。


由以上图片可以看出当namespace定义在函数内部时,即不是定于在全局时就会发生报错。但可以嵌套定义。


由以上两张图片可知,namespace定义命名空间在不同的文件定义相同的名字,并不会发生报错。
5.3 命名空间使用
编译查找变量的定义/声明时,只会在局部或者全局寻找,不会在命名空间内寻找。如果我们要使用命名空间内的变量/函数,有以下几种方法:
- 指定命名空间访问,制作项目时推荐;
- using将命名空间中某个成员展开,项目中经常访问的不存在冲突的成员推荐这种方式;
- 展开命名空间中全部成员,冲突风险太大,项目不推荐,平时练习可以使用。
这三种方法按照解析程度我们可以对比一下由点到线再由线到面的思想,逐层递进。接下来将依次介绍上述三种方法的用法实例。
5.3.1. 指定命名空间访问
在了解学习指定命名空间访问之前,我们需要先来学习一个符号::,它是域解析运算符,通过它我们可以访问全局命名空间的成员。

可知命名空间不可直接进行访问。
加上域解析运算符便可进行访问,用法如上,命名空间名::需要解析的变量/函数;当然,你也可以通过域解析运算符来访问全局变量,用法为:::需要解析的变量/函数。简单来说就是域解析运算符前不用加上命名空间名。
5.3.2 using将命名空间中某个成员展开
1. namespace sl
2. {
3. int a = 10;
4. int Add(int x, int y)
5. {
6. return x + y;
7. }
8. }
9. using sl:: a;
10. int main()
11. {
12. printf("%d\n", a);
13. return 0;
14. }
在外部将命名空间中某个变量或函数使用using展开后,我们再次访问该变量时便可不再使用域解析运算符就可直接进行访问。
5.3.3 展开命名空间中全部成员
1. namespace sl
2. {
3. int a = 10;
4. int Add(int x, int y)
5. {
6. return x + y;
7. }
8. }
9. using namespace sl;
10. int main()
11. {
12. printf("%d\n", Add(3, 4));
13. return 0;
14. }
由代码可知,using将命名空间中全部成员展开,不再使用域解析运算符便可访问命名空间中的成员。
6.C++输入&输出
由C++第一个程序,我们便可得知,C++有一套区别于C的输入输出语法。
接下来我们将详细介绍以下几点。
- C++程序把输入和输出看做字节流。输入时,程序从输入流中抽取字节;输出时,程序将字节插入到输出流中。对于面对文本的程序,每个字节代表一个字符,更通俗地说,字节可以构成字符或数据的二进制表示。输入流中的 字节可能来自键盘,也可能来自存储设备(如硬盘)或其他程序。同样,输出流中的字节可以流向屏幕、打印机、存储设备或其他程序。流充当了程序和流源或流目标之间的桥梁。这使得C++程序可以以相同的方式对待来自键盘的输入和来自文件的输入。C++只是检查字节流,而不需要知道字节流来自何方。
- <iostream>是Input Output Stream的缩写,是标准的输入、输出流库,定义了标准的输入、输出对象。在此用法中,可以近似认为相当于C中的<stdio.h>.
- std::cin是istream类的对象,它主要面对窄字符的标准输入流。我们在程序中写入的常量,整形等数据都会被转换为字符。
- std::cout是ostream类的对象,它主要面对窄字符的标准输出流。
- std::endl是一个函数,流输入输出时,相当于插入一个换行字符加刷新缓冲区。
- <<是流插入运算符,>>是流提取运算符。(C语言还用这两个运算符做位运算左移/右移)
- 使用C++输入输出更加方便,不需要像printf/scanf输入输出那样,需要手动指定格式,C++的输入输出可以自动识别变量类型(本质是通过函数重载实现的)其实最重要的是C++的流能更友好的支持自定义类型对象的输入输出。
- cout/cin/endl等都属于C++标准库,C++标准库都放在一个叫std的命名空间中,所以要通过命名空间的使用方法去用他们。
- C++标准库有时会包含一些C的函数,例若包含<iostream>则可在VS上使用C的printf和scanf,但是不同的编译器包含情况是不同的。
- 若要控制输出小数点后位数和控制精度,建议使用C的函数来进行处理,一是因为C在这一方面较为简单,而是因为C++兼容C,C的输入输出函数在C++也可使用。
注:一般情况下我们输入的内容只有在内存中才会有整型,原反补等类型;而在中端处理器,控制台中都是字符。
1. #include<iostream>
2. using namespace std;//平时练习中我们可以直接解析std
3. int main()
4. {
5.
6. int a = 10;
7. char c = 'a';
8. double d = 1.0;
9. cout << a <<" "<< c <<" "<< d << endl;
10. cin >> a;
11. cin >> c >> d;
12. cout << a << " " << c << " " << d << endl;
13. scanf("%d",&a);
14. printf("%d\n",a);
15.
16. return 0;
17. }
可以看出,C++输入输出流可以自己识别类型,无论是整型,双精度浮点型,还是字符型都会自己识别。 并且printf,scanf在C++中也可以使用,这是因为<iosteram>包含了<stdio.h>,相当于间接包含了C的输入输出,但不同编译器不同,在其他编译器上可能报错。
并且,无论是C++还是C我们再输入数据或者输出设备时,不会直接传到控制台,而是先放在一个特定的区域,这个区域称为缓冲区。但接收到刷新命令时,数据流会被传到控制台。无论是cout,cin,还是printf,scanf,他们都有各自的缓冲区,不会冲突。但这种效率低,需要付出更多的时间成本。并且C++中输入输出流有时还有绑定作用,输入的刷新命令可能连带着输出一块传到控制台,这都增加了时间成本。有时我们在处理多个数据时,或者参加一些竞赛时,可能通不过,这时候我们就需要输入以下语句,使效率提高。
1. #include<iostream>
2. using namespace std;
3. int main()
4. {
5. ios_base::sync_with_stdio(false);//这行代码是斩断和c的输入输出联系
6. cin.tie(nullptr);//以下两行代码是斩断C++的输入输出联系
7. cout.tie(nullptr);
8. return 0;
9. }
7.缺省参数
相信了解过C++的同学都听说过一句话,做人不要做缺省参数,为什么这样说呢。原来缺省参数的特点就是需要他时他存在,不需要时他就没用了,基本对应现实中的舔狗。接下来就让我们一起来学习一下缺省参数的特点和使用方法吧。
7.1 缺省参数概念
- 缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
- 通俗点讲,就是在定义函数的时候可以给形参赋一个初始化的值,这个值就叫做缺省值,缺省值可以有一个,也可以有多个。
- 按照给定缺省值占比的不同,缺省又可分为全缺省和半缺省。顾名思义,全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。
7.2 缺省参数使用要点
- C++规定半缺省必须从右往左以此连续缺省,不能间隔跳跃给缺省值。
- 带缺省参数的函数调用,C++规定必须从左到右依次给实参,不能跳跃给实参。
- 函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值。
1. #include<iostream>
2. using namespace std;
3. int Add(int x, int y=10)
4. {
5. return x + y;
6. }
7. int main()
8. {
9. cout << Add(3) << endl;
10. return 0;
11. }
这就是一个典型的半缺省函数,从右向左给缺省值。
//Stack.h
1. #pragma once
2. #include<stdio.h>
3. #include<stdlib.h>
4. #include<assert.h>
5. #include<stdbool.h>
6. typedef int STDataType;
7. typedef struct Stack
8. {
9. STDataType* arr;
10. int top;
11. int capacity;
12. }ST;
13. void STInit(ST* ps, int n);
14. //Stack.cpp
15. #include"Stack.h"
16. void STInit(ST* ps, int n =10)
17. {
18. assert(ps && n > 0);
19. ps->arr = (STDataType*)malloc(n * sizeof(STDataType));
20. ps->top = 0;
21. ps->capacity = n;
22. }
23. //test.cpp
24. int main()
25. {
26. ST st;
27. STInit(&st);
28. }


以上图片和代码说明当定义和声明分离时,定义和声明不能同时给值,并且只能在函数声明时给值,否则在调用时就会查找不到给值,引起报错。
对于此函数,缺省函数的优点在于若提前知道要开多大的空间,便可直接给值,不引起扩容浪费,若不知道,也可暂时给一个小值。
8. 函数重载
C++支持在同一作用域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者类型不同。这样C++函数调用就表现出了多态行为,使用更灵活。C语言是不支持同一作用域中出现同名函数的。
1. 参数类型不同
1. #include<iostream>
2. using namespace std;
3. int Add(int x, int y)
4. {
5. return x + y;
6. }
7. double Add(double x, double y)
8. {
9. return x + y;
10. }
11. int main()
12. {
13. int a = Add(2, 3);
14. double b = Add(2.0, 3.0);
15. cout << a << endl;
16. cout << b << endl;
17. return 0;
18. }
2. 参数个数不同
1. void f()
2. {
3. cout << "f()" << endl;
4. }
5. void f(int a)
6. {
7. cout << "f(int a)" << endl;
8. }
9. int main()
10. {
11. f();
12. f(2);
13. return 0;
14. }
3. 参数类型顺序不同(实质是类型不同)
1. void f(int a,char c)
2. {
3. cout << "f(int a,char c)" << endl;
4. }
5. void f(char c,int a)
6. {
7. cout << "f(char c,int a)" << endl;
8. }
9. int main()
10. {
11. f(1,'a');
12. f('a',2);
13. return 0;
14. }
注:返回值不同不能作为重载条件,因为调用时无法区分。
1. void f()
2. {
3. cout << "f()" << endl;
4. }
5. void f(int a=10)
6. {
7. cout << "f(int a)" << endl;
8. }
9. int main()
10. {
11. f();
12. f(2);
13. return 0;
14. }
这个函数在语法上构成函数重载,但第二个函数还是缺省函数,调用时会引发歧义,发生报错。

9. 引用
在C语言中我们学习了指针,但是一级还好,到二级我们就开始头疼了,有时搞不懂了。C++为了简化这一方面,就引入了引用这一概念。引用简单来说并不是给一个变量再取一个名字,而是取别名,其和原来的变量共用同一块内存空间。
引用用法:
类型& 引用别名 = 引用对象
1. int main()
2. {
3. int a = 0;
4. int& b = a;
5. int& c = b;
6. int& d = c;
7. ++d;
8. cout << &a << endl;
9. cout << &b << endl;
10. cout << &c << endl;
11. cout << &d << endl;
12. }

引用可以连续引用,并且其打印的地址是一样的 ,这也可以证明其和原来的变量共用同一块内存空间。
9.1 引用的特性
- 引用在定义时必须初始化;
- 引用一旦引用一个实体,再不能引用其他实体;


由图可知,当引用在定义时不初始化的话就会发生报错。并且当变量已引用一个对象后,就不可再引用另一个对象。
总结可知:引用在一定程度上可以取代指针,并且比指针更加好用,也更简单,但并不是可以完全替代指针。有些方面只能用指针,比如在链表实现中,每个节点中都存在下一个链表的地址,这里如果我们用引用的话,引用在指定一个对象后就不可方生改变。但在java中可以,并且其没有指针的概念。
9.2 引用的应用
- 引用传参跟指针传参是类似的,所以指针能做的事引用大部分也能做,并且引用传参更为方便;
- 引用在实践中主要是用于引用传参和引用做返回值中减少拷贝提高效率和改变引用对象同时改变被引用对象;
- 一些主要用C语言代码实现数据结构中使用C++引用替代指针传参,目的是简化程序;
1. void Swap(int& x, int& y)
2. {
3. int tmp = x;
4. x = y;
5. y = tmp;
6. }
7. int main()
8. {
9. int a = 10;
10. int b = 1;
11. cout << a << " " << b << endl;
12. Swap(a, b);
13.
14. cout << a << " " << b<<endl;
15. }
这是用引用来实现两值交换,可以看出引用比指针简洁了不少,这也是引用的第二点特性:改变引用对象同时改变被引用对象;
1. struct A
2. {
3. int arr[1000];
4. };
5. void Func(A& aa)
6. {
7. }
8. int main()
9. {
10. A aa1;
11. Func(aa1);
12. }
这里如果用传值传参,相当于要拷贝4000个字节,而使用引用或者指针则不需要,都是同一块空间;
当然,引用也可以用于返回值,比如出堆;C++在传值传参返回时会有一个拷贝,不会直接返回,而是开辟一个空间存零时对象,零时对象具有常性,所以使用传值传参时不可以直接修改堆中元素;传引用返回返回的是别名,本质还是它本身,所以可以修改;中间不存在零时对象;
9.3 const引用
我们在学c的时候也有一个const修饰指针,可以类比一下;
- 可以引用一个const对象,但是必须用const引用。const引用也可以引用普通对象,因为对象的访问权限在引用过程中可以缩小,但是不能放大;
- 需要注意的是类似Int&b=a*3;double d=12.34;int& rd=d;这样一些场景下a*3的和结果保存在一个临时对象中,int& rd=d也是类似,在类型转换中会产生临时对象存储中间值,也就是说,b和rd引用的都是临时对象,而C++规定临时对象具有常性,所以这里就触发了权限放大,必须要常引用才可以;
- 所谓临时对象就是编译器需要一个空间暂存表达式的求值结果时临时创建的一个未命名的对象,C++把这个未命名对象叫做临时对象;


权限可缩小,不能放大;
9.4 指针和引用的关系
刚刚已经很多次讲解过指针和引用很相似,功能上有很多重叠之处,但却都不可替代;
- 语法概念上引用是一个变量的取别名不开空间,指针是存储一个变量地址,要开空间;
- 引用在定义时必须初始化,指针建议初始化,但是语法上不是必须的;
- 引用在初始化时引用一个对象后,就不可再引用其他对象;而指针可以在不断的改变指向对象;
- 引用可以直接访问指向对象,指针需要解引用才是访问指向对象;
- sizeof中含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8个字节)
- 指针很容易出现空指针和野指针的问题,引用很少出现,引用使用起来相对安全;

底层汇编角度看,引用也是由指针实现的。
10.nullptr
NULL实际上是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
1. #ifndef NULL
2. #ifdef __cplusplus
3. #define NULL 0
4. #else
5. #define NULL ((void *)0)
6. #endif
7. #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只能被隐式地转换为指针类型,而不能被转换为整数类型。
1. #include<iostream>
2. using namespace std;
3. void f(int x)
4. {
5. cout << "f(int x)" << endl;
6. }
7. void f(int* ptr)
8. {
9. cout << "f(int*ptr)" << endl;
10. }
11. int main()
12. {
13. f(0);
14. //本想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义为0,调用了f(int x),因此15. 与程序的初衷相悖。
16. f(NULL);
17. f((int*)NULL);
18.
19. //编译报错:错误 C2665 “f” : 没有重载函数可以转换所有参数类型
20. //f((void*)NULL);
21. f(nullptr);
22. return 0;
23. }
11.inline
内联函数是C++的增强特性之一,用来降低程序的运行时间。当内联函数收到编译器的指示时,即可发生内联:把内联函数的函数体在编译器预处理的时候替换到函数调用处(加副本),这样代码运行到这里时候就不需要花时间去调用函数(减少了函数调用过程的入栈出栈等开销),注意这种替代行为发生在编译阶段而非程序运行阶段,且对内联函数进行任何修改,都需要重新编译函数的所有客户端,因为编译器需要重新更换一次所有的代码,否则将会继续使用旧的函数。
- 用inline修饰的函数叫做内联函数,编译时C++编译器会在调用的地方展开内联函数,这样调用内联函数就不需要建立栈帧了,并且其改良了define的漏洞,可以进行调试,并且在定义函数时也不如define那样难以表达;
- inline对于编译器而言只是一个建议,也就是说,你加了inline编译器也可以选择在调用的时候不展开,不同编译器对于inline什么情况展开各不相同,因为C++标准没有规定这个。inline适用于频繁调用的短小函数,对于递归函数,代码相对多一点的函数,加上inline也会被编译器忽略;
- vs编译器debug版本下面默认是不展开inline的,这样方便调试,debug版本想展开需要设置一下;
- inline不建议声明和定义分离到两个文件,分离会导致链接错误。因为inline被展开,就没有函数地址,连接时会出现报错;
- 函数被编译起来以后是一堆指令,需要存储出来执行,第一句指令的地址就是函数的地址;
inline展开
1. #include<iostream>
2. using namespace std;
3. inline int Add(int x, int y)
4. {
5. int ret = x + y;
6.
7. ret += 1;
8. ret += 1;
9. ret += 1;
10. ret += 1;
11. return ret;
12. }
13. int main()
14. {
15. int ret = Add(1, 2);
16. return 0;
17. }

inline不展开
1. #include<iostream>
2. using namespace std;
3. inline int Add(int x, int y)
4. {
5. int ret = x + y;
6. ret += 1;
7. ret += 1;
8. ret += 1;
9. ret += 1;
10. ret += 1;
11. ret += 1;
12. ret += 1;
13. ret += 1;
14. ret += 1;
15. ret += 1;
16. ret += 1;
17. ret += 1;
18. ret += 1;
19. ret += 1;
20. ret += 1;
21. ret += 1;
22. ret += 1;
23. ret += 1;
24. return ret;
25. }
26. int main()
27. {
28. int ret = Add(1, 2);
29. return 0;
30. }
由以上展开和不展开对比可知,不展开建立栈帧更少,假如一个程序中调用了10000条某个函数指令,而该函数共100行,那么不展开总共就是10100行,而展开就是10000*100行,这差距还是蛮大的。
1万+

被折叠的 条评论
为什么被折叠?



