文章目录
- 前言
- 1. C++关键字
- 2. C++的第一个程序
- 3. 命名空间
- 3.1 namespace的定义
- 3.2 命名空间的嵌套
- 3.3 命名空间使用
- 3.4 查找优先级总结
- 4. C++输入和输出
- 4.1 标准输入输出 (iostream库)
- 4.2 文件输入输出 (fstream库)
- 4.3 字符串流 (sstream库)
- 4.4 C++格式化输出
- 4.5 std::endl和\n的区别
- 5. 缺省参数
- 5.1 全缺省
- 5.2 半缺省
- 5.3 总结
- 6. 函数重载
- C++支持函数重载的原理
- 7. 引用
- 7.1 引用特性
- 7.2 const引用(常引用)
- 7.1 易混淆知识点
- 7.2 使用场景
- 7.3 传值和传引用的区别
- 7.4 引用和指针的区别
- 8. 内联函数
- 特性
- 9. auto关键字
- 9.1 使用场景
- 9.2 注意事项
- 10. 范围for循环
- 11. 空指针nullptr
- END
前言
C++是在C语言的基础上发展而来的一种面向对象编程语言,它在保留C语言强大功能的同时,引入了许多新的特性。比如面向对象编程(OOP):类, 封装, 继承, 多态四大件,引用(Reference),模板(Template),命名空间(Namespace),异常处理(Exception Handling),标准模板库(STL)
C++在很多领域有着广泛的应用,比如游戏开发,操作系统开发,嵌入式系统开发, 数据库管理系统,图形图像处理和网络通信(QT),虚拟现实(VR)和增强现实(AR),航空航天与军工领域等等
1. C++关键字
在C++98/03标准中,C++共有63个关键字(如下图所示)。然而,随着C++11、C++14、C++17以及C++20等后续标准的发布,C++的关键字数量逐渐增加。
具体来说,C++11新增了10个关键字,包括alignas、alignof、char16_t、char32_t、constexpr、decltype、noexcept、nullptr、static_assert和thread_local。这些新增的关键字为C++提供了更多的功能和灵活性。
2. C++的第一个程序
#include<iostream>
using namespace std;
int main()
{
cout << "hello world\n" << endl;
return 0;
}
这里先简单了解一下,后面会详细解释
3. 命名空间
在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
简单举个例子,rand()函数是在头文件<stdlib.h>中的,它的作用是产生一个随机数
如图,当没有包含头文件<stdlib.h>时程序可以正常运行,打印全局变量rand的值
如果包含头文件<stdlib.h>,程序还会不会正常运行呢?
发现程序报错了,因为将头文件<stdlib.h>展开的话就会有rand()函数与全局变量rand重名,此时它们都在全局域内,这就会产生重定义。所以命名空间namespace就应运而生了。
3.1 namespace的定义
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对花括号{}即可,{}中即为命名空间的成员。命名空间中可以定义变量/函数/类型等。
namespace本质是定义出一个域,这个域跟全局域各自独立,不同的域可以定义同名变量,所以下面的rand就不在冲突了
在这里定义一个命名空间MY,在命名空间MY里面定义一个rand变量,此时就不会和头文件<stdlib.h>中的rand()函数冲突了,通过MY::rand就可以将值打印出来了。
这里的::是作用域运算符,使用命名空间名称加作用域运算符就可以访问命名空间内的元素。
同时也可以访问全局rand函数指针
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
namespace MY {
int rand = 10;
}
int main()
{
// 这⾥默认是访问的是全局的rand函数指针
printf("%p\n", rand);
// 这⾥打印MY命名空间中的rand
//printf("%d\n", MY::rand);
return 0;
}
可以正常打印全局rand函数的地址
3.2 命名空间的嵌套
命名空间嵌套在编程中是一种组织代码的有效方式,特别是在处理大型项目或库时。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
namespace MY {
int rand = 10;
int Add(int left, int right)
{
return (left + right) * 10;
}
namespace my
{
int rand = 20;
int Add(int left, int right)
{
return (left + right) * 20;
}
}
}
int main()
{
printf("%d\n", MY::rand);
printf("%d\n", MY::Add(1, 2));
printf("%d\n", MY::my::rand);
printf("%d\n", MY::my::Add(1, 2));
return 0;
}
注意:命名空间namespace只能定义在全局
- 匿名命名空间:匿名命名空间是C++中的一种特殊命名空间,其主要特点是没有名称,且其中的变量和函数只能在声明它们的文件中访问。这种命名空间的使用在C++编程中非常有用,尤其是在处理全局变量和函数时,可以避免命名冲突,并将它们的作用范围限制在当前文件中,从而提高代码的可读性和可维护性。
namespace {
int internal_variable = 42; // 匿名命名空间中的变量
void internal_function() {
// 匿名命名空间中的函数
}
}
- 命名空间合并
项目工程中多文件中定义的同名命名空间(namespace)会认为是一个命名空间(namespace),不会冲突。
3.3 命名空间使用
编译查找一个变量的声明/定义时,默认只会在局部或者全局查找,不会到命名空间里面去查找。所以我们要使用命名空间中定义的变量/函数,有三种方式:
- 指定命名空间访问,项目中推荐这种方式。
- using将命名空间中某个成员展开,项目中经常访问的不存在冲突的成员推荐这种方式。
- 展开命名空间中全部成员,项目不推荐,冲突风险很大,日常小练习程序为了方便推荐使用。
指定命名空间访问
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
namespace MY {
int a = 0;
int b = 1;
}
int main()
{
// 指定命名空间访问
printf("%d\n", MY::a);
return 0;
}
使用using将命名空间中某个成员展开
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
namespace MY {
int a = 0;
int b = 1;
}
//using将命名空间中某个成员展开
//using MY::a;
int main()
{
using MY::a;
printf("%d\n", a);
printf("%d\n", MY::b);
return 0;
}
展开命名空间中全部成员
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
namespace MY {
int a = 0;
int b = 1;
}
//展开命名空间中全部成员
//using namespace MY;
int main()
{
using namespace MY;
printf("%d\n", a);
printf("%d\n", b);
return 0;
}
展开命名空间中全部成员这种写法是很容易造成重定义的问题,比如在全局再定义一个变量a(a=10),会发生如下报错:
但是如果在局部域定义一个变量a(a=10),则会优先输出在局部定义的变量a
3.4 查找优先级总结
- 在没有使用using声明或using指令展开命名空间的情况下,查找优先级为:函数局部域 > 全局域 > (未展开的)命名空间域。
- 如果使用了using声明或using指令来展开命名空间,则展开后的命名空间域中的名称将与全局域中的名称具有相同的查找优先级(但仅限于被展开的部分),但仍然低于函数局部域的优先级。
4. C++输入和输出
C++中的输入输出(I/O)是编程中非常基础且重要的部分,它允许程序与外部世界(如用户、文件等)进行交互。C++标准库提供了丰富的I/O功能,主要通过iostream、fstream、sstream等头文件支持。下面将详细介绍这些方面的基本概念和用法。
4.1 标准输入输出 (iostream库)
在C++中,输入流(Input Streams)和输出流(Output Streams)是处理数据输入和输出的核心概念。它们代表了数据从程序外部(如键盘、文件、网络等)流入程序内部,以及数据从程序内部流向程序外部(如屏幕、文件、网络等)的过程。
标准输入输出库iostream(Input Output Stream 的缩写)提供了对控制台(即命令行或终端)进行输入输出操作的工具。最常用的类包括std::cin(用于输入)和std::cout(用于输出),以及std::cerr和std::clog(用于错误输出,但std::cerr是非缓冲的,即立即输出,而std::clog是缓冲的)。
- std::cout:标准输出流,它主要面向窄字符的标准输出流。用于将数据输出到标准输出设备(通常是屏幕)。它继承自std::ostream类,并提供了一系列成员函数来支持数据的输出操作。
- std::cin:标准输入流,它主要面向窄字符的标准输入流。用于从标准输入设备(通常是键盘)读取数据。它继承自std::istream类,并提供了一系列成员函数来支持数据的读取操作。
输出:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using std::cout;//将std::cout展开
int main()
{
int a = 0;
double b = 0.1;
char c = 'x';
// <<是流插⼊运算符,>>是流提取运算符。
cout << "Hello World" << std::endl; //打印字符串并换行
std::cout << a << " " << b << " " << c << std::endl;//将a,b,c打印到屏幕上
return 0;
}
输入:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using std::cout;//将std::cout展开
using std::cin;//将std::cin展开
int main()
{
int a ;
double b;
char c;
//C语言的输入输出不能自动识别变量的类型
scanf("%d%lf", &a, &b);
printf("%d %lf\n", a, b);
// 可以⾃动识别变量的类型
// <<是流插⼊运算符,>>是流提取运算符。
cin >> a;
cin >> b >> c;
cout << a << std::endl;
cout << b << " " << c << std::endl;
return 0;
}
C++的输入输出和C语言的输入输出对比
灵活性:
C语言的输入输出用scanf函数和printf函数来完成,需要手动指定变量类型(格式)。使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动指定格式,可以使用<<流插入运算符和>>流提取运算符来自动识别。但C++的格式化输出相对复杂一些,虽然可以使用操纵符来进行格式化输出,但与printf的格式化字符串相比,可能需要更多的代码来实现相同的效果。
安全性:
C语言的printf()和scanf()函数在类型安全性方面存在不足。例如,使用%d格式符错误地输出或输入非整型数据(如浮点数或字符串)时,程序可能无法正确运行或产生不可预测的结果。而C++的I/O操作是类型安全的。编译器会对I/O操作进行严格的类型检查,确保数据类型与操作符或函数参数的类型相匹配。这种特性有助于减少因类型不匹配而导致的错误。
性能分析:
C语言:
通常情况下,C语言的输入输出函数在性能上相对较高。这是因为它们被设计为非常轻量级的函数调用,直接与底层操作系统的输入输出机制进行交互,减少了一些额外的开销。
C++:
在某些情况下,性能可能不如 C 语言的输入输出函数。这是因为iostream库在内部进行了更多的类型检查和缓冲管理等操作,会带来一些额外的开销。
C++的缓冲区管理:C++的iostream库默认与C标准库的stdio缓冲区同步,这允许在C++程序中混合使用scanf/printf和cin/cout。但这种同步可能会带来额外的性能开销。
如果确定在程序中不会混合使用这两套I/O系统,可以通过调用std::ios_base::sync_with_stdio(false);来关闭同步,从而显著提高cin/cout的性能。
在IO需求比较高的地方,如部分大量输入的竞赛题中,加上以下3行代码,可以提高C++IO效率。
#include<iostream>
using namespace std;//日常训练和竞赛的时候可以全部展开,大型项目不推荐
int main()
{
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
return 0;
}
4.2 文件输入输出 (fstream库)
文件输入输出库fstream提供了对文件进行读写操作的工具。它包括了std::ifstream(输入文件流)、std::ofstream(输出文件流)和std::fstream(同时支持输入输出的文件流)三个类。
- std::ifstream:文件输入流,用于从文件中读取数据。它继承自std::istream类,并添加了用于文件操作的功能,如打开文件、读取文件内容等。
- std::ofstream:文件输出流,用于将数据写入到文件中。它继承自std::ostream类,并添加了用于文件操作的功能,如打开文件、写入文件内容等。
写入文件:
#include <fstream>
#include <iostream>
int main() {
std::ofstream outFile("example.txt"); // 创建输出文件流对象,并尝试打开example.txt 新的内容会覆盖原来的内容
if (outFile.is_open()) {
outFile << "This is a line.\n"; // 写入一行文本
outFile << "This is another line.\n";
outFile.close(); // 关闭文件
} else {
std::cout << "Unable to open file";
}
return 0;
}
如果不想让新的内容覆盖原来的内容,可以以追加模式打开文件,具体如下:
std::ofstream outFile("example.txt",std::ios_base::app);
读取文件:
#include <fstream>
#include <iostream>
#include <string>
int main() {
std::ifstream inFile("example.txt"); // 创建输入文件流对象,并尝试打开example.txt
std::string line;
if (inFile.is_open()) {
while (getline(inFile, line)) { // 逐行读取
std::cout << line << '\n';
}
inFile.close();
} else {
std::cout << "Unable to open file";
}
return 0;
}
4.3 字符串流 (sstream库)
字符串流库sstream允许你以流的方式对字符串进行读写操作,包括std::istringstream(输入字符串流)、std::ostringstream(输出字符串流)和std::stringstream(同时支持输入输出的字符串流)。
输入字符串流:
#include <iostream>
#include <sstream>
#include <string>
int main() {
std::string str = "123 45.6 hello";
std::istringstream iss(str);
int num;
double dbl;
std::string word;
iss >> num >> dbl >> word; // 解析字符串
std::cout << "Number: " << num << std::endl;
std::cout << "Double: " << dbl << std::endl;
std::cout << "Word: " << word << std::endl;
return 0;
}
如果输入的字符串无法正确转换为期望的数据类型,istringstream的读取操作会失败,可以通过检查流的状态来进行错误处理:
#include <iostream>
#include <sstream>
#include <string>
int main() {
std::string str = "abc 45.6 hello";
std::istringstream iss(str);
int num;
double dbl;
std::string word;
if (iss >> num >> dbl >> word) {
std::cout << "Number: " << num << std::endl;
std::cout << "Double: " << dbl << std::endl;
std::cout << "Word: " << word << std::endl;
} else {
std::cerr << "Error reading from stringstream." << std::endl;
}
return 0;
}
输出字符串流:
#include <iostream>
#include <sstream>
#include <string>
int main() {
std::ostringstream oss;
oss << "Number: " << 42 << ", Double: " << 3.14 << ", Word: " << "hello"; // 使用 << 操作符向 oss 中写入了一系列数据
std::string result = oss.str();
std::cout << result << std::endl;
return 0;
}
输入字符串流和输出字符串流结合使用:
#include <iostream>
#include <sstream>
#include <string>
int main() {
std::istringstream iss("10 20");
int a, b;
iss >> a >> b;
std::ostringstream oss;
oss << "Sum: " << a + b;
std::cout << oss.str() << std::endl;
return 0;
}
4.4 C++格式化输出
在 C++ 中,iomanip头文件中的一些函数如setprecision、setw等可以用于格式化输出。
std::setprecision用于设置浮点数输出时的精度(即小数点后的位数)。它可以与std::fixed或std::scientific结合使用,以指定是以固定的小数点表示法还是以科学记数法表示浮点数。
#include <iostream>
#include <iomanip> // 必须包含此头文件以使用setprecision等操作符
int main() {
double pi = 3.141592653589793;
std::cout << std::fixed << std::setprecision(3) << pi << std::endl; // 输出:3.142
std::cout << std::scientific << std::setprecision(5) << pi << std::endl; // 输出:3.14159e+00
return 0;
}
setw(n)的功能:设置输出字段的宽度。如果输出内容小于字段宽度,会在内容前面填充空格(默认)以达到指定宽度。不过,你可以通过std::left(向左对齐)、std::right(默认向右对齐)和std::internal(在数值的符号(如果有的话)和数值的其余部分之间插入填充字符(通常是空格))来控制填充的位置。
#include <iostream>
#include <iomanip>
int main() {
int number = 42;
std::cout << std::setw(10) << number << std::endl; // 输出: 42(默认右对齐)
std::cout << std::setw(10) << std::left << number << std::endl; // 输出:42 (左对齐)
return 0;
}
setfill(n)的功能:设置用于填充输出字段的字符。通常与setw一起使用。
#include <iostream>
#include <iomanip>
int main() {
int num = 10;
std::cout << std::setfill('*') << std::setw(5) << num << std::endl; // 输出***10
return 0;
}
结合使用std::setw和std::setprecision来控制输出格式:
#include <iostream>
#include <iomanip>
int main() {
double numbers[] = { 1.23456, 7.89012, 3.14159 };
for (double num : numbers) {
std::cout << std::setw(10) << std::fixed << std::setprecision(3) << num << std::endl;
}
return 0;
}
注意:
- std::setw、std::setprecision等操作符仅影响紧随其后的下一个输出项。如果你想要对多个输出项应用相同的格式设置,你需要在每个输出项之前重复这些操作符。
- 这些操作符通常与std::iomanip头文件一起使用,因为std::iomanip是包含这些操作符定义的头文件。
- 在使用这些操作符时,确保包含了正确的头文件,并在需要时设置了正确的输出格式标志(如std::fixed、std::scientific、std::left、std::right等)。
4.5 std::endl和\n的区别
功能方面:
‘\n’:
1.仅仅是一个换行符,它将输出位置移动到下一行的开头。
2.只负责换行操作,不会刷新输出缓冲区。
std::endl:
1.std::endl 是一个函数,流插入输出时,除了插入一个换行符外,还会刷新输出缓冲区。
2.刷新输出缓冲区意味着立即将缓冲区中的内容输出到目标设备(如显示器、文件等),确保输出及时显示或保存。
使用场景:
- 使用std::endl的场景:
当需要确保每次输出后立即将数据传递给底层系统时(例如,实时日志记录或向用户及时显示信息),可以选择使用std::endl。
在调试过程中,为了实时看到输出结果,也常使用std::endl。- 使用\n的场景:
在不需要立即刷新缓冲区的场合,使用\n可能会提供更好的性能。
当进行大量输出操作时,使用\n可以减少因频繁刷新缓冲区而导致的性能损失。
在必要时,可以手动调用std::flush或其他刷新机制来清空缓冲区。
性能方面:
- 如果对性能要求较高且不需要立即刷新输出缓冲区,使用’\n’会比使用std::endl效率更高。因为频繁刷新输出缓冲区可能会带来额外的开销,特别是在大量输出的情况下。
- 如果需要确保输出立即显示或保存,或者担心程序在输出过程中出现异常导致缓冲区中的内容丢失,使用std::endl更合适。
5. 缺省参数
概念:缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定的实参则采用该形参的缺省值,否则使用指定的实参。(有些地方把缺省参数也叫默认参数)
函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须在函数声明中给缺省值。因为如果缺省参数在函数声明和定义中同时出现并且给的缺省值都不同的话,编译器就无法确定缺省值到底是哪一个,所以统一在函数声明中给缺省参数。
注意:C语言不支持缺省参数,只能通过一些技巧来模拟,比如使用宏定义(使用宏定义了缺省值,调用函数时传入一个特定的值,再将缺省值赋给形参即可),使用函数重载(定义多个不同参数的函数来模拟缺省参数的效果)。
缺省参数又分为全缺省和半缺省参数,具体如下
5.1 全缺省
概念:全缺省就是全部形参给缺省值
#include <iostream>
#include <assert.h>
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;
}
int main()
{
Func1();
Func1(1);
Func1(1, 2);
Func1(1, 2, 3);
return 0;
}
由此可见,在带缺省参数的函数被调用时,C++规定必须从左往右依次给实参,不能跳跃给实参。
5.2 半缺省
概念:半缺省就是部分形参给缺省值。
#include <iostream>
#include <assert.h>
using namespace std;
// 半缺省
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()
{
Func2(1);
Func2(1, 2);
Func2(1, 2, 3);
return 0;
}
注意:
C++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。
以下都是错误的使用
5.3 总结
- 缺省参数不能在函数声明和定义中同时出现
- 带缺省函数被调用时,从左往右依次给实参,同时需要注意的是:无缺省形参的位置必须传实参
- 缺省值必须常量或全局变量(如果缺省值是一个局部变量,那么在不同的函数调用中,这个局部变量的值可能会不同,导致函数的行为不一致。)
- C语言本身不支持缺省参数,但可以使用宏定义或函数重载来实现这个功能。
6. 函数重载
概念:在 C++ 中,函数重载是一种允许在同一个作用域内定义多个同名函数,但这些函数的参数列表不同的特性。参数列表不同可以分为参数数量不同,参数类型不同,参数顺序不同
#include<iostream>
using namespace std;
// 1、参数类型不同
void Add(int a, int b)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
}
void Add(double a, double b)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl << endl;
}
// 2、参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl << endl;
}
// 3、参数类型顺序不同(等同于参数类型不同)
void f1(int a, char b)
{
cout << "f(int a,char b)" << endl;
}
void f1(char b, int a)
{
cout << "f(char b, int a)" << endl << endl;
}
int main()
{
Add(10, 20);
Add(10.1, 20.2);
f();
f(10);
f1(10, 'a');
f1('a', 10);
return 0;
}
注意:当两个函数的函数名相同时,如果两个函数的参数列表相同而返回类型不同,这种情况也不会构成函数重载。因为函数在被调用时,编译器不知道调用哪一个
再看一个编译器不知道调用哪个函数的例子:
#include<iostream>
using namespace std;
void f1()
{
cout << "f()" << endl;
}
void f1(int a = 10)
{
cout << "f(int a)" << endl;
}
int main()
{
//f1(); 不能正常运行,编译器不知道调用哪一个函数
f1(1);//可以正常运行
return 0;
}
上面两个函数构成重载,但调用f1()时,会报错,存在歧义,编译器不知道调用谁
注意:在不同的作用域中,两个函数就算同名,参数列表相同还有返回类型相同,那也是不同的两个函数,不会造成冲突。
#include<iostream>
using namespace std;
void f()
{
cout << "f()" << endl;
}
namespace MY {
void f()
{
cout << "f()" << endl;
}
}
int main()
{
f();
MY::f();
return 0;
}
C++支持函数重载的原理
为什么C语言不支持函数重载?——>因为在编译过程中函数名修饰的差异
- 在C语言中,函数名直接用于链接,没有额外的修饰,导致无法区分同名函数。即使两个函数具有相同的名称但参数列表不同,在C语言中也会被视为重复定义,导致编译错误
- 在 C++ 中,为了支持函数重载,编译器采用了名称修饰机制。当编译器遇到一个函数声明时,会根据函数的名称和参数列表生成一个唯一的内部名称。这个内部名称包含了函数的参数信息,使得不同参数列表的同名函数在链接时可以被正确区分。
例如,对于两个重载的函数 void func(int) 和 void func(double),编译器可能会生成类似于 _func_int 和 _func_double 这样的内部名称。在链接阶段,根据函数调用的参数类型,链接器可以准确地找到对应的函数实现。
C语言的函数名没有修饰
#include<stdio.h>
void f(int a);
int main()
{
f(1);
return 0;
}
C++的函数名修饰
#include<iostream>
using namespace std;
void f(int a, double b);
void f(double a,int b);
int main()
{
f(1,1.2);
f(1.2,1);
return 0;
}
可以清楚地看到两个函数的内部名称和地址是不相同的,所以编译器可以正确地区分它们。
为什么会报错?
在C++中,调用一个函数时程序在编译和链接阶段要找到函数的实现,以及在执行阶段能够有确切的指令来执行函数调用。而函数的地址其实就是函数内第一条指令的地址,在函数只有声明而没有定义的情况下,这个函数就没有地址并且找不到指令来执行函数调用,因此才会报错。
7. 引用
概念:引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
C++的引用符号复用了C语言的取地址符号&。这里是为了避免引入太多的运算符,所以会复用C语言的一些符号,比如>>和<<
具体使用如下:
类型& 引用别名 = 引用对象;
注意:引用变量也可以被引用,都是一开始被引用变量的别名
#include<iostream>
using namespace std;
int main()
{
int a = 0;
//引用:b和c是a的别名
int& b = a;
int& c = b;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
cout << endl;
//也可以给别名b取别名,d相当于还是a的别名
int& d = b;
cout << "d = " << d << endl;
cout << endl;
++d;
//++d,相当于a,b,c也++
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
cout << "d = " << d << endl;
cout << endl;
//b,c,d都是a的别名,都共享一块内存空间,取地址都是一样的
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &d << endl;
return 0;
}
注意:引用与被引用的对象类型必须一致
被引用的对象类型可以是指针
7.1 引用特性
1. 引用在定义时必须初始化
2. 一个变量可以有多个引用
3. 引用一旦引用一个实体,再不能引用其他实体
#include<iostream>
using namespace std;
int main()
{
int a = 10;
int& b = a;
int c = 20;
// 这里并非让b引⽤c,因为C++引用不能改变指向,
// 这里是⼀个赋值
b = c;
cout << a << endl;
cout << b << endl;
cout << endl;
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
return 0;
}
7.2 const引用(常引用)
const引用是一种特别的引用类型,当用const引用类型去引用一个对象时,只能通过别名去访问该对象,而不能通过修改别名的值去修改所引用对象的值。因为引用类型是常量,不能修改,只能通过修改所引用对象的值去修改别名的值。
#include<iostream>
using namespace std;
int main()
{
int a = 10;
const int& b = a;
b = 20;
cout << a << endl;
cout << b << endl;
return 0;
}
不能修改常引用的值
只能通过修改被引用对象的值去修改常引用的值
注意:可以引用一个const对象,但是必须用const引用。const引用也可以引用普通对象,因为对象的访问权限在引用过程中可以缩小,但是不能放大。
#include<iostream>
using namespace std;
int main()
{
const int a = 10;
const int& b = a;
//因为引用c是int类型,而引用对象a是const int类型,这里触发了权限放大,所以报错
int& c = a;
return 0;
}
当引用的对象是const int类型(常量整数类型,不可修改),引用是int类型(整数类型,可以修改)时,如果引用的对象都不能修改,那么就不可能通过修改引用的值去修改它所绑定的对象的值
下面看个例子:
为什么b引用a可以,c引用a*3就不行?下面会详细介绍
为什么 int& b = a; 是合法的?
int a = 10; 声明了一个整型变量a并初始化为10。int& b = a; 声明了一个对a的引用b。这里,b是a的别名,它们共享相同的内存地址。对b的任何非const修改都会直接反映到a上,因为b只是a的另一种访问方式。
为什么 int& c = a * 3; 是不合法的?
a * 3 是一个表达式,它计算a和3的乘积,并产生一个临时对象来存储临时的整型值(在这个例子中是30)。这个临时对象没有持久的内存地址,它在表达式求值完成后就会被丢弃。因此,你不能将一个引用绑定到一个临时对象上,因为引用需要绑定到一个持久的、可以唯一标识的实体上。
总结来说,int& b = a; 是合法的,因为a是一个持久的、可以唯一标识的整型变量。而int& c = a * 3; 是不合法的,因为a * 3的结果保存在一个临时对象中,没有持久的内存地址。可以先将a *3 的结果保存在整型变量(如temp)中,然后再对这个整型变量创建引用。
所谓临时对象就是编译器需要一个空间暂存表达式的求值结果时临时创建的一个未命名的对象,C++中把这个未命名对象叫做临时对象。
也可以将 int& c = a * 3; 改为 const int& c = a* 3; 同样是合法的
常量引用有一个重要的用途:它可以延长临时对象的生命周期。在C++中,通常临时对象的生命周期仅限于包含它们的完整表达式。但是,如果你将一个临时对象绑定到一个常量引用上,那么这个临时对象的生命周期就会被延长,直到引用它的常量引用超出作用域或被重新绑定到另一个对象上。
下面再看一个例子:
int& rd = d; 会报错
const int& rd = d; 不会报错
为什么int& rd = d; 会报错?
在 int& rd = d; 中,试图将一个 int 类型的引用 rd 绑定到一个 double 类型的对象 d。这是不允许的,因为 int 和 double 是不同的类型,没有直接的兼容性。如果可以将 int 引用绑定到 double 对象,那么通过 int 引用对对象进行修改时,可能会破坏 double 对象的内部结构,导致不可预测的结果。
为什么const int& rd = d; 不会报错?
当使用 const int& rd = d; 时,编译器可以创建一个临时的 int 对象来进行类型转换(隐式类型转换)并让 const 引用绑定到这个临时对象,因为 const 引用可以绑定到临时对象,而普通的非 const 引用不能。由于引用 rd 是 const 的,它延长了临时对象的生命周期,也保证了临时对象在其生命周期内不会被意外修改。
7.1 易混淆知识点
int const* p 和 const int* p 在C和C++中的含义是完全相同的,没有区别。它们都表示一个指向const int的指针,即指针p指向一个整数,但这个整数是不可修改的。这里的const关键字用来修饰指针所指向的数据,而不是指针本身。
而int* const p与它们不同:
- int* const p:这个声明表示 p 是一个常量指针,指向一个整数(int)。这里的 const 修饰的是指针 p 本身,而不是指针所指向的整数。因此,你不能通过重新赋值来改变 p 指向的地址,但你可以通过 p 来修改它所指向的整数值。
简而言之:
- int* const p:指针是常量,指向的整数可以修改。
- const int* p(或 int const* p):指针不是常量,但指向的整数是常量。
示例1:const int* p 或 int const* p
#include<iostream>
using namespace std;
int main() {
int x = 10;
int y = 20;
const int* p = &x; // p 是一个指向常量整数的指针,指向 x
// 修改 p 指向的地址(可以)
p = &y; // 现在 p 指向 y
// 尝试通过 p 修改所指向的整数的值(错误)
// *p = 5; // 这将导致编译错误,因为 p 指向的是 const int
cout << p << endl;
cout << &y << endl;
return 0;
}
这个例子中,我们有一个指向常量整数的指针p。我们可以改变p指向的地址,但我们不能通过p来修改它所指向的整数的值。
示例2:int* const p
#include<iostream>
using namespace std;
int main() {
int x = 10;
int y = 20;
int* const p = &x; // p 是一个常量指针,指向 x
// 尝试修改 p 指向的地址(错误)
// p = &y; // 这将导致编译错误
// 修改 p 指向的整数的值(可以)
*p = 5; // 现在 x 的值是 5
cout << p << endl;
cout << &x << endl;
cout << &y << endl;
cout << endl;
cout << *p << endl;
return 0;
}
这个例子中,我们有一个常量指针p,它指向一个整数。我们不能改变p指向的地址,但我们可以修改p所指向的整数的值。
7.2 使用场景
作函数的参数
可以简单的实现一下交换函数
#include<iostream>
using namespace std;
void Swap(int& a, int& b)
{
int temp = a;
a = b;
b = temp;
}
int main()
{
int x = 1, y = 2;
Swap(x, y);
cout << x << " " << y << endl;
return 0;
}
在之前实现数据结构——单链表时,尾插操作使用了二级指针
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//定义链表(结点)的结构
typedef int SLTDataType;
typedef struct SListNode {
SLTDataType data;
struct SListNode* next;
}SLTNode;
//申请新结点
SLTNode* SLTbuyNode(SLTDataType x)
{
//使用malloc创建新结点,因为malloc的返回值类型是void*,使用这里还需要强行转换为SLTNode*指针类型
SLTNode* nownode = (SLTNode*)malloc(sizeof(SLTNode));
if (nownode == NULL)//申请失败
{
perror("malloc fail!");//打印错误信息
exit(1);//异常退出
}
//申请成功
nownode->data = x;
nownode->next = NULL;
return nownode;//返回这个新结点
}
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);//传入的地址不能为空
SLTNode* nownode = SLTbuyNode(x);//申请新结点
if (*pphead == NULL)//链表为空的情况
{
*pphead = nownode;//申请的新结点就是头结点
}
else
{
//创建新变量pcur将*pphead赋值给pcur,再进行循环遍历操作,这样头结点指针不会发生变化
SLTNode* pcur = *pphead;
//找到尾结点
while (pcur->next)
{
pcur = pcur->next;
}
pcur->next = nownode;//将尾结点的next指向node,尾插成功
}
}
学习了引用之后就可以用它替代二级指针:SLTNode*& pphead,用这个来作为对实参的引用
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//定义链表(结点)的结构
typedef int SLTDataType;
typedef struct SListNode {
SLTDataType data;
struct SListNode* next;
}SLTNode;
//申请新结点
SLTNode* SLTbuyNode(SLTDataType x)
{
//使用malloc创建新结点,因为malloc的返回值类型是void*,使用这里还需要强行转换为SLTNode*指针类型
SLTNode* nownode = (SLTNode*)malloc(sizeof(SLTNode));
if (nownode == NULL)//申请失败
{
perror("malloc fail!");//打印错误信息
exit(1);//异常退出
}
//申请成功
nownode->data = x;
nownode->next = NULL;
return nownode;//返回这个新结点
}
//尾插
void SLTPushBack(SLTNode*& pphead, SLTDataType x)
{
assert(pphead);//传入的地址不能为空
SLTNode* nownode = SLTbuyNode(x);//申请新结点
if (pphead == NULL)//链表为空的情况
{
pphead = nownode;//申请的新结点就是头结点
}
else
{
//创建新变量pcur将*pphead赋值给pcur,再进行循环遍历操作,这样头结点指针不会发生变化
SLTNode* pcur = pphead;
//找到尾结点
while (pcur->next)
{
pcur = pcur->next;
}
pcur->next = nownode;//将尾结点的next指向node,尾插成功
}
}
也可以如下使用:
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//定义链表(结点)的结构
typedef int SLTDataType;
typedef struct SListNode {
SLTDataType data;
struct SListNode* next;
}SLTNode, * SLNode;
//申请新结点
SLTNode* SLTbuyNode(SLTDataType x)
{
//使用malloc创建新结点,因为malloc的返回值类型是void*,使用这里还需要强行转换为SLTNode*指针类型
SLTNode* nownode = (SLTNode*)malloc(sizeof(SLTNode));
if (nownode == NULL)//申请失败
{
perror("malloc fail!");//打印错误信息
exit(1);//异常退出
}
//申请成功
nownode->data = x;
nownode->next = NULL;
return nownode;//返回这个新结点
}
//尾插
void SLTPushBack(SLNode& pphead, SLTDataType x)
{
//assert(pphead);//传入的地址不能为空
SLTNode* nownode = SLTbuyNode(x);//申请新结点
if (pphead == NULL)//链表为空的情况
{
pphead = nownode;//申请的新结点就是头结点
}
else
{
//创建新变量pcur将*pphead赋值给pcur,再进行循环遍历操作,这样头结点指针不会发生变化
SLTNode* pcur = pphead;
//找到尾结点
while (pcur->next)
{
pcur = pcur->next;
}
pcur->next = nownode;//将尾结点的next指向node,尾插成功
}
}
做返回值
(1) 返回参数的引用
#include<iostream>
using namespace std;
int& addOne(int& num) {
num++;
return num;
}
int main() {
int x = 5;
int& result = addOne(x);
cout << result << endl; // 输出6
return 0;
}
这个例子中,函数addOne接受一个整数的引用作为参数,并返回这个引用,在函数内对参数进行修改后,通过返回的引用可以直接获取修改后的值。
(2) 返回局部变量的引用(不安全)
#include <iostream>
using namespace std;
int& test(int a) {
return a;
}
int main() {
int a = 10;
int& ref = test(a);
cout << ref << endl;
cout << ref << endl;
return 0;
}
在test函数中,参数a是在函数调用时在栈上分配的内存空间。当函数执行完毕后,这个局部变量的内存空间会被回收,此时返回它的引用就会导致未定义行为。
两次输出 ref 可能会出现不可预期的结果,甚至可能导致程序崩溃,因为引用指向的内存已经无效。
(3) 返回静态变量的引用
#include <iostream>
using namespace std;
int& test() {
static int a = 20;
return a;
}
int main() {
int& ref = test();
cout << "ref = " << ref << endl;
ref = 1000;
cout << ref << endl;
return 0;
}
在这个例子中,test 函数返回一个静态局部变量 a 的引用。静态局部变量在程序的整个运行期间都存在,因此返回它的引用是安全的。在 main 函数中,通过引用 ref 可以安全地访问并修改 a 的值。
(4) 返回const 引用修饰的函数
#include <iostream>
using namespace std;
const int& test() {
static int a = 10;
return a;
}
int main() {
const int& ref = test();
cout << ref << endl;
cout << ref << endl;
return 0;
}
ref 引用是const int类型(常量整数型),所以不能修改
(5) 返回成员变量的引用
#include <iostream>
using namespace std;
class MyClass {
public:
int& getValue() {
return data;
}
private:
int data = 5;
};
int main() {
MyClass obj;
int& ref = obj.getValue();
cout << obj.getValue() << endl;
ref = 10;
cout << obj.getValue() << endl;
return 0;
}
getValue函数返回成员变量data的引用,通过这个引用可以修改对象中的数据。
7.3 传值和传引用的区别
概念:
- 传值:传值是将实参的值复制一份传递给函数的形参。在函数内部对形参的修改不会影响到实参的值。
- 传引用:传引用是将实参的地址传递给函数的形参,形参实际上是实参的一个别名。在函数内部对形参的修改会直接影响到实参的值。
内存使用:
- 传值:需要为形参分配新的内存空间来存储实参的值,特别是对于大型的对象或结构体,可能会消耗较多的内存进行复制操作。
- 传引用:不需要复制实参的值,只需要传递实参的地址,因此不会产生额外的内存开销。
效率:
- 传值:如果实参是大型对象,复制操作可能会比较耗时,特别是在频繁调用函数的情况下,会降低程序的性能。
- 传引用:由于不需要进行复制操作,函数调用的效率更高,特别是对于大型对象或频繁调用的函数,可以显著提高程序的运行速度。
使用场景:
- 传值:当不希望函数内部的修改影响到实参时,可以使用传值。对于小型的数据类型或者不需要在函数内部修改实参的情况,传值比较简单直观。
- 传引用:当需要在函数内部修改实参的值时,传引用是一个很好的选择。对于大型对象或者结构体,为了提高效率,避免复制操作,可以使用传引用。在一些需要返回多个值的情况下,可以通过传引用的方式将结果存储在实参中返回。
7.4 引用和指针的区别
- 引用:引用是一个已有对象的别名,必须在初始化时绑定到一个对象,并且不能重新绑定到其他对象。用法:类型& 引用别名 = 引用对象;
- 指针:指针是存储另一个对象地址的变量,可以为nullptr,表示不指向任何对象,并且可以重新指向不同的对象。用法:类型* 指针变量名
从底层实现(汇编)来看,引用也是通过指针来实现的
引用和指针的不同点:
-
1.引用是一个变量的取别名,不开空间;指针是存储一个变量地址,要开空间。
-
2.引用在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
-
3.引用在初始化时引用一个对象后,就不能再引用其他对象;而指针可以不断地改变指向的对象。
-
4.引用可以直接访问被引用的对象,指针需要解引用后才可以访问指向的对象。
-
5.没有NULL引用,但有NULL指针
-
6.sizeof中含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8byte)
-
7.引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
-
8.有多级指针,但是没有多级引用
-
9.引用比指针使用起来相对更安全
引用在定义时必须初始化,并且一旦初始化后就不能再重新绑定到其他对象。这意味着引用总是指向一个有效的对象,不会出现悬空引用的情况。而指针可以不初始化,或者在运行过程中被赋值为nullptr,如果不小心使用未初始化的指针或者指向已释放内存的指针(悬空指针)进行解引用操作,就会导致程序崩溃或出现不可预测的行为。
引用由于其必须初始化、不能重新指向其他对象以及无需解引用操作等特性,使得它在使用上相对指针更加安全,减少了出现内存错误和程序崩溃的风险。
8. 内联函数
用inline修饰的函数叫做内联函数,编译时C++编译器会在调用的地方展开内联函数,这样调用内联函数就不需要建立栈帧了,就可以提高效率。
我们可以通过汇编观察内联函数是否展开,有 call 语句就是没有展开,没有就是展开了
#include<iostream>
using namespace std;
int Add(int x, int y)
{
int ret = x + y;
ret += 1;
ret += 1;
ret += 1;
return ret;
}
int main()
{
int ret = Add(1, 2);
return 0;
}
可以看到在调用Add函数的地方,看到有call Add语句,所以Add函数不是内联函数
#include<iostream>
using namespace std;
inline int Add(int x, int y)
{
int ret = x + y;
ret += 1;
ret += 1;
ret += 1;
return ret;
}
int main()
{
int ret = Add(1, 2);
return 0;
}
由图可以看出没有call Add语句,说明内联函数已经展开,即Add函数是内联函数
vs编译器 debug版本下面默认是不展开inline的,这样方便调试,debug版本想展开需要设置一下以下两个地方。
首先鼠标右击解决方案,点击最下面的属性
点击C/C++的常规,将调试信息格式的右边改为程序数据库(/Zi)
再点击C/C++的优化,将内联函数扩展改为只适用于 _inline(/Ob1)
C语言宏函数的实现
C语言实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不方便调试,C++设计了inline目的就是替代C的宏函数。
下面看一下实现一个宏函数的例子:
#define ADD(a, b) ((a) + (b))
实现ADD宏函数的常见问题
#include<iostream>
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))
// 1.为什么不能加分号?
// 2.为什么要加外面的括号?
// 3.为什么要加里面的括号?
int main()
{
int ret = ADD(1, 2);
cout << ADD(1, 2) << endl;
//1.如果加了分号,ADD(1, 2)相当于((a) + (b)); 因为;是一条语句结束的地方,输出的时候会报错
cout << ADD(1, 2) * 5 << endl;
//2.如果不加外面的括号,ADD(1, 2)相当于(a) + (b),此时的结果为1+2*5=11,不是预想的结果为15
int x = 1, y = 2;
ADD(x & y, x | y);
//3.如果不加里面的括号,ADD(1, 2)相当于(a + b),此时结果为(x&y+x|y),而符号+比符号&和|的优先级高,会先计算y+x的和,这样结果就不对了
return 0;
}
宏函数的优缺点
- 优点
高效性:宏函数在预处理阶段直接进行文本替换,没有函数调用的开销,执行速度快。相比函数调用,它不需要在运行时进行参数传递和返回值处理等操作,节省了时间和空间资源。
灵活性:可以接受不同类型的参数,不像普通函数那样严格受限于参数类型。宏函数可以根据不同的条件进行不同的文本替换,从而实现更加灵活的代码生成。
方便调试:在调试过程中,宏函数的展开是在预处理阶段进行的,可以直接看到宏函数展开后的代码,方便理解程序的执行流程和进行调试。
- 缺点
代码膨胀:由于宏函数是在预处理阶段进行文本替换,所以如果在程序中多次使用宏函数,会导致代码膨胀。
缺乏类型检查:宏函数在进行文本替换时,不会进行类型检查。这可能会导致一些潜在的错误,例如参数类型不匹配、运算结果超出预期范围等。
可读性差:宏函数的定义通常比较复杂,难以理解。特别是一些复杂的宏函数,可能需要使用多个参数和嵌套的表达式,这使得代码的可读性大大降低。
可维护性差:宏函数的定义一旦发生变化,可能会影响到程序中的多个地方。这使得代码的维护变得困难,特别是在大型项目中,可能需要花费大量的时间来查找和修改受影响的代码。
特性
- 1.inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,这样做可能会使目标文件变大,但是少了函数调用的开销,提高程序运行效率
- 2.inline对于编译器只是一个建议,当inline向编译器发出一个请求时,编译器可以忽略这个请求。inline适用于频繁调用的短小函数,对于递归函数,代码相对多⼀些的函数,加上inline也会被编译器忽略。
- 3.inline不建议声明和定义分离到两个文件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错。
注意:当编译器决定对inline函数进行内联展开时,该函数的代码被直接嵌入到调用它的每个位置,就不需要函数地址跳转到函数体了,因此inline函数不再作为一个独立的、可寻址的函数实体存在。
9. auto关键字
概念:在C++中,auto关键字的主要作用是进行自动类型推断。这意味着当使用auto来声明变量时,编译器会根据初始化表达式自动确定该变量的类型。
当使用auto自动识别变量类型的时候,可以使用typeid(要识别类型的对象).name() 来获取一个对象的类型信息
typeid操作符:
- typeid 是一个在运行时(runtime)进行类型识别的操作符。
- 它可以用于任何类型的对象或类型名(包括类类型、内置类型、枚举类型等)。
- typeid 返回一个 std::type_info 对象,该对象包含了关于类型的信息。
name 成员函数:
- std::type_info 类有一个成员函数 name(),它返回一个指向以 null 结尾的字符数组的指针,该数组包含了类型名的字符串表示。
- 这个字符串的具体形式依赖于实现(即编译器和标准库的实现),不同的编译器可能会返回不同的字符串。
#include<iostream>
#include<string>
#include<map>
#include<vector>
using namespace std;
int Add()
{
return 10;
}
int main()
{
auto x = 5; // x的类型是int
auto y = 3.14; // y的类型是double
auto z = "Hello"; // z的类型是const char*(在C++11及更高版本中)
auto a = 'x'; //a的类型是chat
auto add = Add(); //add的类型是add
vector<string> t;
map<int, char> mp;
cout << typeid(x).name() << endl << endl;
cout << typeid(y).name() << endl << endl;
cout << typeid(z).name() << endl << endl;
cout << typeid(a).name() << endl << endl;
cout << typeid(add).name() << endl << endl;
cout << typeid(t).name() << endl << endl;
cout << typeid(mp).name() << endl << endl;
return 0;
}
使用auto定义新变量时,等号右边的变量必须是已初始化的。auto不是一种类型的声明,而是一种类型的占位符,当编译器根据初始化表达式自动推断出变量的类型时,auto再替换成该类型。
9.1 使用场景
auto与指针结合
这里p的类型被自动推导为int*,即指向整数的指针。对于复杂的数据结构,auto同样能准确推导指针类型。这里的auto和auto是一致的,都是指针类型,但是auto只能识别指针类型的变量。
auto与引用结合
#include<iostream>
using namespace std;
int main()
{
int num = 10;
int& ref = num;
auto re = ref;
auto& r = ref;
return 0;
}
这里r的类型被自动推导为int&,即对整数的引用。而re只是int类型,因为如果没有明确指定引用,auto会自动去除引用属性进行类型推导。所以当auto声明引用类型时必须加&
使用auto定义多个变量
当使用auto定义多个变量时,编译器只对第一个变量的类型进行识别,剩下的变量默认为该类型
当这些变量的类型不一样的时候会报错
9.2 注意事项
-
- auto不能作为函数的参数。
- auto不能作为函数的参数。
-
- auto不能直接用来声明数组
- auto不能直接用来声明数组
10. 范围for循环
概念:在C++中,范围for循环(也称为基于范围的for循环)是一种简洁且易于理解的循环结构,用于遍历数组、容器(如std::vector、std::list等)或其他可迭代对象中的所有元素。这种循环结构在C++11标准中被引入,旨在简化传统的基于迭代器的循环代码。
基本语法
for (auto element : range) {
// 执行的代码块,其中element是当前迭代的元素
}
- element:这是循环中每次迭代时使用的变量,它会被自动初始化为range中的当前元素。auto关键字用于自动推导变量的类型。每次循环,element 会被初始化为 range 序列中的下一个元素。
- range:这是要遍历的可迭代对象,比如数组、std::vector、std::list等
执行过程
- 初始化:在每次迭代开始时,element变量会被初始化为range中的下一个元素。
- 迭代:循环会遍历range中的所有元素,每次迭代都会执行循环体内的代码块。
- 终止:当range中的所有元素都被遍历过时,循环终止。
遍历数组:
#include<iostream>
using namespace std;
int main()
{
int arr[5] = { 1,2,3,4,5 };
//num是arr数组里的元素,arr是被遍历的数组,auto自动识别变量类型
for (auto num : arr)
{
cout << num << " ";
}
return 0;
}
遍历容器:
#include<iostream>
#include<vector>
using namespace std;
int main()
{
vector<int> vec = { 6, 7, 8, 9, 10 };
for (auto value : vec)
{
cout << value << " ";
}
return 0;
}
使用条件
for循环迭代的范围必须是确定的,对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于容器而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
如果要修改原始容器中的元素,应该使用迭代器或引用。
使用迭代器:
#include<iostream>
#include<vector>
using namespace std;
int main()
{
vector<int> vec = { 6, 7, 8, 9, 10 };
for (auto value : vec)
{
cout << value << " ";
}
cout << endl;
vector<int>::iterator it;
for (it = vec.begin(); it < vec.end(); it++)
{
*it += 10;
cout << *it << " ";
}
return 0;
}
迭代器的本质:迭代器是一种类似指针的对象,它指向容器中的元素。在这个循环中,it是一个迭代器,它指向容器中的某个元素的位置。通过解引用操作符*,可以获取迭代器所指向的实际元素。就如同通过指针解引用可以访问指针所指向的内存地址中的值一样。
使用引用:
#include<iostream>
#include<vector>
using namespace std;
int main()
{
vector<int> vec = { 6, 7, 8, 9, 10 };
for (auto value : vec)
{
cout << value << " ";
}
cout << endl;
for (auto& it : vec)
{
it += 10;
cout << it << " ";
}
return 0;
}
优点
简洁性:相比传统的 for 循环,范围 for 循环更加简洁直观,减少了循环变量的初始化、条件判断和更新等繁琐的操作。
安全性:可以避免因循环变量的错误操作而导致的越界访问等问题。
通用性:适用于各种可迭代的数据结构,提高了代码的可重用性。
11. 空指针nullptr
背景:在C++11之前,通常使用NULL或0来表示空指针。然而,NULL实际上是一个宏定义,可能被定义为0或((void*)0)。这种定义方式在某些情况下可能会导致类型混淆和错误,因为整数0可能会被隐式地转换为其他类型的指针,从而引起潜在的类型安全问题。为了解决这些问题,C++11引入了nullptr关键字。
在传统的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);调用会报错。
#include<iostream>
using 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(intx),因此与程序的初衷相悖。
f(NULL);
//只能将NULL转换成int*类型才能调用指针版本的f(int*)函数
f((int*)NULL);
return 0;
}
调用f((void*)NULL);会发生错误,因为从 void* 到指向非 void 的指针的强制转换要求显式类型强制转换
C++11中引入nullptr,nullptr是一个特殊的关键字,nullptr是一种特殊类型的字面量,它可以转换成任意其他类型的指针类型。使用nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,而不能被转换为整数类型。
注意事项
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
- 在C++11及以后的版本中,推荐使用nullptr来表示空指针,以提高代码的可读性和安全性。
- 避免将nullptr与整数类型进行比较或赋值,以防止类型不匹配的问题。
END
到这里就结束了,相信大家已经对C++的语法有了简单的认识,内容比较长,需要记忆的地方和细节处也比较多,建议整理出笔记方便以后复习。对以上内容有什么建议和需要补充的地方欢迎大家来讨论!