作者主页:lightqjx
本文专栏:C++
目录
一、C++中的关键字
二、C++的输入&输出
三、缺省参数
1. 概念
2. 全缺省参数
3. 半缺省参数
4. 声明和定义
四、函数重载
五、引用
1. 基本概念
2. 引用的特性
3. 引用的使用场景
(1)做函数参数
(2)做函数返回值
4. 常引用
5. 指针与引用的区别
六、内联函数
1. 概念
2. 特性
七、auto关键字(C++11)
1. 简介
2. 使用细则
3. 不能推导的情况
八、基于范围的for循环(C++11)
九、指针空值nullptr(C++11)
一、C++中的关键字
C++(C++98)中总计63个关键字,而C语言中有32个关键字。C++98 是 C++ 语言的第一个标准化版本,它正式确立了 C++ 的核心语言特性和标准库,为后续的 C++ 标准奠定了基础。如图是C++(C++98)中的63个关键字展示:
其中namespace已在【C++】命名空间(namespace关键字)-CSDN博客中介绍。
二、C++的输入&输出
在C++中,输入输出的代码简单示例如下:
- #include <iostream>
- using namespace std;
- int main()
- {
- int a;
- cin >> a;// 输入
- cout << "输入的值:" << a << endl;// 输出该变量并换行
- cout << "Hello, World!" << endl; // 输出字符串并换行
- return 0;
- }
复制代码
运行结果:
说明:
- 在C++中,cout是标准输出对象(控制台),用于输出;cin是标准输入对象(键盘),用于输入。使用cout和cin必须包含< iostream >头文件以及按命名空间使用方法使用std。
- cout和cin是全局的流对象。endl是特殊的C++符号,表示换行输出。他们都包含在包含< iostream >头文件中。
- << 是流插入运算符,>> 是流提取运算符。
比较:
使用C++输入输出更方便,不需要像C语言中的printf/scanf输入输出时那样,需要手动控制格式。C++的输入输出可以自动识别变量类型。如以下代码:
- #include <iostream>
- using namespace std;
- int main()
- {
- int a;
- double b;
- char c;
- // cin 和 cout 都可以自动识别变量的类型
- cin >> a;
- cin >> b >> c;
- cout << a << endl;
- cout << b << " " << c << endl;
- return 0;
- }
复制代码
三、缺省参数
1. 概念
缺省参数是声明或定义函数时为函数的参数指定一个缺省值,缺省值即默认值。在调用该函数时,如果没有传递实参则采用该形参的缺省值(默认值),如果传递实参则使用该传递的实参。如以下代码:
- #include <iostream>
- using namespace std;
- void Func(int a = 0) //该参数的默认值为0
- {
- cout << a << endl;//输出参数
- }
- int main()
- {
- Func(); // 没有传参时,使用参数的默认值
- Func(10); // 传参时,使用指定的实参
- return 0;
- }
复制代码
运行结果:
C++缺省参数的情况可以分为两种情况:全缺省参数和半缺省参数。
2. 全缺省参数
全缺省参数的情况是指要定义的函数中的所有参数都有一个缺省值(默认值)。如以下代码:
- #include <iostream>
- using namespace std;
- void Func(int a = 10, int b = 20, int c = 30)
- {
- cout << "a = " << a << " ";
- cout << "b = " << b << " ";
- cout << "c = " << c << endl;
- }
- int main()
- {
- Func();
- Func(11);
- Func(11,22);
- Func(11,22,33);
- return 0;
- }
复制代码
运行后:
这种在全缺省参数的情况中调用函数时,传递的参数是从左向右进行的,先对函数中左边的a进行赋值,再依次对右边的参数b,c进行赋值。
3. 半缺省参数
半缺省参数的情况是指要定义的函数中的并不是所有参数都有一个缺省值(默认值),并且缺省的参数是从右向左进行缺省的,也就是说,如果有缺省,最右边的参数一定会有默认值。如以下代码:
- #include <iostream>
- using namespace std;
- //半缺省参数 -- 右向左缺省
- void Func(int a, int b = 20, int c = 30) // 只对b,c进行了缺省
- {
- cout << "a = " << a << " ";
- cout << "b = " << b << " ";
- cout << "c = " << c << endl;
- }
- int main()
- {
- Func(11);
- Func(11, 22);
- Func(11, 22, 33);
- return 0;
- }
复制代码
运行后:
当然这里传参也是从左向右进行传参的,不能指定传参。
观察一下可以发现,这里没有调用没传参的函数了,因为如果调用没传参的函数,就会语法出错,和C语言一样会报错。
因此,调用函数时,要根据函数定义的情况,保证未被缺省的参数一定要传递得到参数,防止错误。
4. 声明和定义
在 C++ 中,缺省参数(默认参数) 可以在 函数声明 或 函数定义 中指定,但 不能同时在两者中指定,否则会导致编译错误。
原因:如果声明与定义位置同时出现缺省参数,而恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值
声明函数后,再调用函数,如果声明和定义相同,即同时指定缺省参数,编译后如图所示:
虽然默认参数在声明和定义中不能同时指定,但是我们通常不会采用只在函数定义中指定默认参数,我们推荐的是只在函数声明中指定默认参数。
在 C++ 中,默认参数(缺省参数)应该只在函数声明中指定(通常位于头文件),而不在函数定义中重复指定。这是最推荐的写法。其详细原因如下:
- 如果函数声明和定义分离(例如头文件声明 + 源文件定义),且默认参数仅出现在定义中,外部代码在调用时无法看到默认参数,导致编译错误:
编译阶段头文件展开,在链接阶段才会将所有文件合起来,如果我们在函数声明是不指定默认参数,而只在函数定义时指定默认参数,这时(以main函数为例)若在main函数中调用该函数,如果你传递完了该函数的所有参数,则你的编译器就可能不会报错;但是如果你只传递了部分参数,则就会发生编译错误,因为此时是处在编译阶段,在main函数所处的文件中只有没有默认参数的函数声明,因此它就会和C语言的语法一样,报一个关于函数参数数目不足的编译错误。
四、函数重载
1. 基本概念
在 C++ 中,函数重载 是指在同一个作用域内,可以定义多个同名函数,但它们的参数列表必须不同(参数类型、参数数量或参数顺序不同),常用来处理实现功能类似数据类型不同的问题,编译器会根据调用时提供的参数类型和数量,自动选择最匹配的函数版本执行。
注意:如果函数的参数列表都相同的两个同名函数,但只有函数返回值不同,这种情况不是函数重载。
代码示例:
- #include<iostream>
- using 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 func()
- {
- cout << "f()" << endl;
- }
- void func(int a)
- {
- cout << "f(int a)" << endl;
- }
- // 3、参数类型顺序不同
- void func(int a, char b)
- {
- cout << "f(int a,char b)" << endl;
- }
- void func(char b, int a)
- {
- cout << "f(char b, int a)" << endl;
- }
- int main()
- {
- Add(10, 20);
- Add(10.1, 20.2);
- func();
- func(10);
- func(10, 'a');
- func('a', 10);
- return 0;
- }
复制代码
可以正常运行:
2. 为什么C++支持函数重载,而C不支持?
在C/C++中,一个程序要运行起来,需要经历:预处理、编译、汇编、链接 这四个阶段。
其中,在编译阶段会生成汇编代码,生成的汇编代码在C/C++中大体上是基本相同的,但也有一点点差异,即:
- C++ 编译器在编译时会对函数名进行修饰,将函数名、参数类型、返回类型等信息编码成一个唯一的符号名。
- C 编译器在编译时直接使用函数名作为符号名,不进行名称修饰。
因此,C只能通过函数名来做区分,则同名函数没办法区分;C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。这一差异体现了 C++ 对编译时多态和类型安全的支持,而 C 更注重简洁性和底层控制。
五、引用
1. 基本概念
在 C++ 中,引用是给已存在变量取一个别名。它可以为已存在的变量提供了一个新的名称,允许通过不同的名字访问同一内存位置,所以,它和它引用的变量共用同一块内存空间,不会申请新的空间。
语法:类型 &引用名 = 变量名;
这里的是对 int 类型变量进行的引用,则它的引用类型是 int& 。
引用是 C++ 特有的特性(C 语言没有引用),它提供了比指针更简洁、安全的语法,同时避免了指针的复杂操作(如解引用 * 和取地址 &)。
2. 引用的特性
使用引用需要注意它的使用特性。
不初始化,编译器会报错。
如以下代码:
- int main()
- {
- int a = 10;
- int& ra = a;
- int& b = a;
- int& c = a;
- return 0;
- }
复制代码
ra,b,c,都是a的引用。这里需要注意的是如果a的任意一个引用进行改变都会影响a的值,因为引用只是对原变量另外取的名字(别名),它们指的都是同一块空间,只是名字不同罢了。
改变其中一个值,其它变量都会改变:
看以下代码:
- int main()
- {
- int a = 10;
- int b = 20;
- int& ra = a;
- ra = b;//这句代码并不是对b进行引用,而是对a进行赋值
- return 0;
- }
复制代码
如果ra是已知变量a的引用,则ra就不能再作为其他变量的引用了,如果将执行ra=b,则是将b的值赋给了ra,也就是对a的这块空间赋值。
3. 引用的使用场景
C++中的引用有两种使用场景:做函数参数和做函数返回值。
(1)做函数参数
引用做参数有两个作用:
- 减少拷贝,提高效率;
- 做输出型参数
在C++中,引用作为函数参数允许函数直接操作实参变量,而不是将形参作为实参的拷贝(复制),所以就减少了拷贝,提高了效率。所以,引用作为参数时,函数内对形参的修改会直接影响实参。如以下实现一个交换的代码:
- #include<iostream>
- using namespace std;
- void Swap(int& left, int& right)
- {
- int temp = left;
- left = right;
- right = temp;
- }
- int main()
- {
- int a = 10;
- int b = 20;
- cout << "a=" << a << " b=" << b << endl;
- Swap(a, b);
- cout << "a=" << a << " b=" << b << endl;
- return 0;
- }
复制代码
其中调用Swap时只需要将a和b两个变量名,不用向C语言那样将传递时进行取地址(&)了。
引用作为函数参数不会开辟新的内存空间,它仅仅是现有变量的一个别名,也就不用再向C语言那样将实参拷贝给形参了,同时提高了时间和空间上的效率。
引用做为函数参数还有一种作用:作输出型参数
在C语言中输出型参数是通过指针实现的,如以下代码:
- #include <stdio.h>
- void add(int a, int b, int* result)
- {
- *result = a + b; // 修改指针指向的值
- }
- int main()
- {
- int sum;
- add(3, 4, &sum); // 传入sum的地址
- printf("%d\n",sum); // 输出: 7
- return 0;
- }
复制代码
其中的 int* result 就是输出型参数。而才C++中输出型参数就是通过引用来实现的,如以下代码:
- #include<iostream>
- using namespace std;
- void add(int a, int b, int& result)
- {
- result = a + b; // 直接修改引用绑定的变量
- }
- int main()
- {
- int sum;
- add(3, 4, sum); // 传入sum的引用
- cout << sum << endl; // 输出: 7
- return 0;
- }
复制代码
其中的 int& result 就是输出型参数。
(2)做函数返回值
引用做函数返回值有两个作用:
- 减少拷贝,提高效率;
- 可以修改返回值 和 获得返回值。
通过C语言学习,我们知道,一般来说,值返回的函数在返回时会先生成一个临时变量,将返回值拷贝到临时变量,再将临时变量拷贝到主函数中获得该函数返回值的变量中去。
如果返回值是引用,则就不需要产生临时变量,也就不用拷贝了,通提高了效率,因为当返回值是引用时,则就是对返回值取别名。
- #include<iostream>
- using namespace std;
- //引用返回的函数
- int& Func()
- {
- static int n = 10;
- return n;
- }
- int main()
- {
- int& ret = Func();
- cout << ret << endl; // 输出: 10
- return 0;
- }
复制代码
做返回值的使用方法:
使用引用返回函数需要注意的是:所调用的函数返回值一定要是在函数结束销毁后仍然存在的变量,如静态变量,动态申请的变量等。因为如果不是这样的变量,那么在函数调用结束后幂函数销毁,此时,函数中的一般变量都会被销毁,那么这样得到的返回值就是随机值了。
如果你得到的仍然是函数中的值,那就是编译器的原因了,因为不同的编译器有不同的效果。如果你的编译器中,运行的值仍为原来的值,n那你可以试试在输出之前在调用一下其他函数,就可以发现问题了:
以上就是对提高效率的解释。
引用做函数返回值的第二个作用:可以获得和修改返回值,使用效果如下:
4. 常引用
在C++中,常引用是一种对引用对象的限制,确保通过该引用无法修改被引用的对象。
首先来看如下代码:
- const int a = 10;//直接用const修饰的变量a
- int& ra = a;//无法对const修饰的变量引用(取别名),该句代码会报错
复制代码
所以,常引用需要注意的第一点:对于const修饰的变量,无法对其进行引用(取别名)。也就是说,引用过程中,权限(这里是指不能从将变量只读变为可读可写)不能放大。
再看以下代码:
- int x = 10;
- int& y = x;
- const int& z = x;//常引用
复制代码
其中y和z都是变量x的别名,但y和z不同的是z进行了常引用,因此,通过z只能对变量x进行读取,不能对x进行修改(权限缩小)。而y这个引用可以对x进行读和改(权限平移)。
- int x = 10;
- int& y = x;
- const int& z = x;//常引用
- //z++;//错误
- y++;
复制代码
常引用需要注意的第二点:被const修饰的引用不能通过它对其数据进行修改,只能读取。也就是说,权限可以平移和缩小。
接下来要讲的是,常引用需要注意的第三点:常引用修饰临时变量(因为临时变量具有常性)。
- double d = 1.11;
- int i = d;
- //类型转换,正常执行,这里进行类型转换时,
- //会先生成一个临时变量将d的值拷贝,再将它拷贝到i中去
- //int& ri = d;//错误,无法执行
- const int& ri = d;//正确
复制代码
正是因为临时变量具有常性,所以普通的引用才会出错,需要进行常引用才行。当然除了这种类型转换时会有临时变量产生,在接收值返回函数的返回值时,收到的也是具有常性的临时变量,也需要注意如果要用引用接收返回值的话,就必须要用常引用。
5. 指针与引用的区别
相同点:
虽然在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间;指针需要新开辟空间。但是它们其实在底层实现上实际都是有空间的,因为引用是按照指针方式来实现的。
代码示例:
- int main()
- {
- int a = 10;
- // 语法层面:不开空间,对a取别名
- int& ra = a;
- ra = 20;
- // 语法层面:开空间,存储a的地址
- int* pa = &a;
- *pa = 30;
- return 0;
- }
复制代码
其中部分反汇编如图所示:
从底层汇编实现的角度来看,引用是通过类似指针的方式实现的,会占用存储空间来存储所引用对象的地址。
不同点:
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求(可以不初始化指针,但绝对不应该这样做)。
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
六、内联函数
1. 概念
以 inline 修饰的函数叫做内联函数。
编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,所以内联函数可以提升程序运行的效率。
实现内联函数的代码示例:
- //内联函数
- inline int Add(int x, int y)
- {
- return x + y;
- }
- int main()
- {
- int a = 10;
- int b = 20;
- int ret = Add(a, b);
- return 0;
- }
复制代码
2. 特性
使用内联函数需要注意三个地方。
- inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
例如:
如果有一个50行指令的函数,而在程序中调用了10000次该函数,。那么,如果该函数不是内联函数,则程序总计调用的不重复的指令就有10000+50行指令;如果该函数是内联函数,则程序总计调用的不重复的指令就有10000*50行指令,远远大于了不是内联函数的指令数,则这里生成的可执行程序就会很大,开销就很大了。
- inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
《C++prime》第五版关于inline的建议:内联说明只是想编译器发出一个请求,编译器可以选择忽略这个请求。
- inline不能像普通函数那样分离声明和定义(例如在头文件声明、源文件定义),分离会导致链接错误。
因为编译器认为内联函数在调用的时候会被展开,它是不会进符号表的,就没有函数地址了,链接时就会找不到。所以,编译器在编译时遇到内联函数调用时,必须能够看到函数的完整定义(包括函数体),否则无法进行内联展开。
七、auto关键字(C++11)
1. 简介
auto是 C++11 引入的一个关键字,用于自动类型推导,它让编译器根据初始化表达式自动推断变量的类型。常常用于简化复杂类型
使用auto需要注意的是:使用auto定义变量时必须对其进行初始化,否则会报错,即:
在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
2. 使用细则
- 用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&。
如以下代码:
- #include<iostream>
- using namespace std;
- int main()
- {
- int x = 10;
- auto a = &x;
- auto* b = &x;// 声明指针类型时,用auto和auto*没有任何区别
- auto& c = x;// 声明引用类型时则必须加&
- cout << "a的类型:" << typeid(a).name() << endl;
- cout << "b的类型:" << typeid(b).name() << endl;
- cout << "c的类型:" << typeid(c).name() << endl;
- return 0;
- }
复制代码
打印类型:
- 当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错。因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量,从而为所有变量推导出一个统一的类型。如果初始化表达式的类型不同,编译器无法确定一个共同的类型,因此会报错。
- int main()
- {
- auto a = 1, b = 2;
- auto c = 3, d = 4; // 该行代码会编译失败。错误:3 是 int,4.0 是 double,无法统一
- return 0;
- }
复制代码
3. 不能推导的情况
但可以推导数组,如果推导数组,则会退化为指针。
- int arr[3] = {1, 2, 3};
- auto a = arr; // a 的类型是 int*,而不是 int[3]
复制代码
以上是关于auto不能推导的常见情况。当然除了这两种情况外,还是有许多情况的。
八、基于范围的for循环(C++11)
1. 基本语法
在C++98中如果要遍历一个数组,常常是按照以下方式进行:
- #include<iostream>
- using namespace std;
- int main()
- {
- int arr[] = { 1, 2, 3, 4, 5 };
- //下标遍历
- for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); ++i)
- {
- cout << arr[i] << endl;
- }
- //指针遍历
- for (int* p = arr; p < arr + sizeof(arr) / sizeof(arr[0]); ++p)
- {
- cout << *p << endl;
- }
- return 0;
- }
复制代码
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。
因此C++11中引入了基于范围的for循环:for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
- #include <iostream>
- using namespace std;
- int main()
- {
- int arr[] = { 1, 2, 3, 4, 5 };
- for (auto e : arr) // 使用基于范围的 for 循环遍历数组
- {
- cout << e << " "; // 输出当前元素
- }
- return 0;
- }
复制代码
上述代码中 “ for (auto e : arr) ” 是基于范围的 for 循环,遍历数组 arr, 自动迭代,自动判断结束。
auto e:auto 让编译器自动推导 e 的类型。由于 arr 是 int[],e 的类型被推导为 int。
e : arr:每次循环,arr 的当前元素会被复制到 e 中(值传递),然后循环会依次访问 数组中每一个元素。并且它会自动判断结束。
需要注意:基于范围的 for 循环与普通循环也是类似的,可以用continue来结束本次循环,也可以用break来跳出整个循环。
2. 使用条件
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供
begin和end的方法,begin和end就是for循环迭代的范围。
九、指针空值nullptr(C++11)
在以前,我们使用的空指针是NULL。但是NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到:
- #ifndef NULL // 如果尚未定义 NULL
- #ifdef __cplusplus // 如果是 C++ 编译器
- #define NULL 0 // 在 C++ 中,NULL 定义为整数 0
- #else // 如果是 C 编译器
- #define NULL ((void *)0) // 在 C 中,NULL 定义为 (void*)0
- #endif
- #endif
复制代码
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何
种定义,在使用空值的指针时,常常会遇到一些麻烦。
nullptr 是 C++11 引入的一个关键字,专门用于表示指针的空值,nullptr 的类型是std::nullptr_t,可以隐式转换为任何指针类型,明确表示“不指向任何对象”,避免传统 NULL 的歧义,取代了传统的 NULL 或 0,以提高代码的安全性和可读性。
使用nullptr需要注意的点:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr 。
感谢各位观看!希望能多多支持!