C++入门

目录

一、命名空间

在C++中,随着程序规模的增大,代码中的标识符(如变量名、函数名、类名等)会越来越多,很容易出现命名冲突。命名空间(Namespace)就是为了解决这一问题而引入的特性,它允许我们将标识符分组到不同的作用域中,从而避免命名冲突。

1. 作用域限定符::

作用域限定符::是C++中用于指定标识符作用域的运算符。当不同作用域中存在同名标识符时,使用::可以明确指定要访问哪个作用域中的标识符。

使用场景

  • 访问全局作用域中的标识符(当局部作用域中存在同名标识符时)。
  • 访问命名空间中的标识符。
  • 访问类中的静态成员或嵌套类型。
#include <iostream>
int x = 10; // 全局变量

int main() {
    int x = 20; // 局部变量,与全局变量同名
    std::cout << "局部变量x: " << x << std::endl;       // 输出20,访问局部作用域的x
    std::cout << "全局变量x: " << ::x << std::endl;     // 输出10,通过::访问全局作用域的x
    return 0;
}

2. 命名空间namespace

命名空间是一个可以包含变量、函数、类、结构体等标识符的作用域,用于将相关的标识符组织在一起,避免命名冲突。

2.1 命名空间的嵌套

命名空间可以嵌套定义,即一个命名空间内部可以包含另一个命名空间。这种嵌套结构可以进一步细化作用域,使代码组织更加清晰。

使用场景:大型项目中,按模块或功能划分嵌套命名空间,避免不同模块间的命名冲突。

namespace A { // 外层命名空间
    int a = 1;
    namespace B { // 嵌套的内层命名空间
        int b = 2;
    }
}

int main() {
    std::cout << A::a << std::endl;      // 输出1,访问外层命名空间A中的a
    std::cout << A::B::b << std::endl;   // 输出2,通过A::B访问嵌套命名空间B中的b
    return 0;
}
2.2 相同命名空间会被合并

如果在程序的不同位置(通常是不同的头文件或源文件中)定义了同名的命名空间,编译器会在编译时将这些同名的命名空间合并为一个完整的命名空间。

使用场景:当一个命名空间中的内容较多时,可以将其拆分到多个文件中定义,最后由编译器自动合并。

namespace MyNamespace { // 第一个定义MyNamespace
    int x = 10;
}

namespace MyNamespace { // 第二个定义MyNamespace,会与第一个合并
    int y = 20; // 合并后,MyNamespace同时包含x和y
}

int main() {
    std::cout << MyNamespace::x << std::endl; // 输出10,访问合并后的MyNamespace中的x
    std::cout << MyNamespace::y << std::endl; // 输出20,访问合并后的MyNamespace中的y
    return 0;
}
2.3 命名空间的全部展开与部分展开

为了简化对命名空间中标识符的访问,可以使用using指令将命名空间中的标识符引入当前作用域。根据引入范围的不同,分为全部展开和部分展开。

全部展开:使用using namespace 命名空间名;将命名空间中的所有标识符引入当前作用域。
注意:全部展开可能导致命名冲突,因此在头文件中应避免使用,通常用于源文件中简化代码。

部分展开:使用using 命名空间名::标识符;只将命名空间中的特定标识符引入当前作用域。
优点:比全部展开更安全,只引入需要的标识符,减少命名冲突的风险。

namespace MyNamespace {
    int a = 1;
    int b = 2;
}

using namespace MyNamespace; // 全部展开:将MyNamespace中的所有标识符引入当前作用域
// using MyNamespace::a;      // 部分展开:只将MyNamespace中的a引入当前作用域

int main() {
    std::cout << a << std::endl; // 输出1,直接访问a(无需MyNamespace::前缀)
    std::cout << b << std::endl; // 输出2(全部展开时可用,部分展开时若未引入b则编译错误)
    return 0;
}

二、C++中的输入输出

C++标准库提供了iostream(输入输出流)库,用于实现控制台输入输出操作。其中,cout(字符输出流)用于标准输出(通常是屏幕),cin(字符输入流)用于标准输入(通常是键盘),它们是C++程序中最常用的输入输出方式。

与C语言的区别:C语言使用printfscanf进行输入输出,而C++的coutcin支持类型安全和运算符重载,使用更灵活、更安全。

#include <iostream> // 包含iostream库,提供输入输出功能

int main() {
    int a; // 定义一个int类型变量a,用于存储输入的值
    std::cout << "请输入一个整数: "; // 使用cout输出提示信息,<<是输出运算符
    std::cin >> a;                   // 使用cin从键盘输入整数到a中,>>是输入运算符
    std::cout << "你输入的整数是: " << a << std::endl; // 输出输入的整数,endl表示换行并刷新缓冲区
    return 0;
}

核心组件说明

  • cout:标准输出流对象,配合<<运算符使用,可输出各种类型的数据(如整数、浮点数、字符串等)。
  • cin:标准输入流对象,配合>>运算符使用,可读取各种类型的数据到变量中。
  • endl:操纵符,用于输出一个换行符并刷新输出缓冲区,确保数据立即显示在屏幕上。
  • std:标准命名空间,coutcinendl等都定义在std命名空间中,因此需要使用std::前缀访问。

三、缺省参数

缺省参数(Default Parameter)是指在函数声明或定义时,为函数的参数指定一个默认值。当函数调用时,如果没有为该参数提供实参,则使用默认值;如果提供了实参,则使用实参的值。缺省参数可以提高函数的灵活性和复用性。

1. 全缺省参数

全缺省参数是指函数的所有参数都指定了默认值。调用这类函数时,可以不传递任何实参(全部使用默认值),也可以传递部分实参(从左到右依次匹配,未传递的参数使用默认值)。

使用规则:实参传递时必须从左到右依次传递,不能跳过前面的参数直接给后面的参数传值。

// 全缺省参数函数:a、b、c都有默认值
void func(int a = 10, int b = 20, int c = 30) {
    std::cout << "a = " << a << ", b = " << b << ", c = " << c << std::endl;
}

int main() {
    func();          // 不传递实参,全部使用默认值,输出a=10, b=20, c=30
    func(1);         // 传递1个实参,a=1,b和c使用默认值,输出a=1, b=20, c=30
    func(1, 2);      // 传递2个实参,a=1, b=2,c使用默认值,输出a=1, b=2, c=30
    func(1, 2, 3);   // 传递3个实参,全部使用实参值,输出a=1, b=2, c=3
    return 0;
}

2. 半缺省参数

半缺省参数是指函数的部分参数指定了默认值。注意:半缺省参数必须从函数参数列表的最右端开始设置,即默认参数必须是连续的,且靠右排列。这是因为函数调用时实参是从左到右传递的,编译器需要知道哪些参数使用默认值。

错误示例void func(int a = 10, int b, int c = 30),这种写法错误,因为默认参数没有从右向左连续设置(b没有默认值,但其右侧的c有默认值)。

// 半缺省参数函数:a没有默认值,b和c有默认值(从右向左连续设置)
void func(int a, int b = 20, int c = 30) { 
    std::cout << "a = " << a << ", b = " << b << ", c = " << c << std::endl;
}

int main() {
    // func();          // 错误:a没有默认值,必须传递实参
    func(1);         // 传递1个实参(a=1),b和c使用默认值,输出a=1, b=20, c=30
    func(1, 2);      // 传递2个实参(a=1, b=2),c使用默认值,输出a=1, b=2, c=30
    func(1, 2, 3);   // 传递3个实参,全部使用实参值,输出a=1, b=2, c=3
    return 0;
}

四、函数重载

在C++中,函数重载(Function Overloading)允许在同一作用域内定义多个同名函数,但这些函数的参数列表(参数类型、参数个数或参数顺序)必须不同。函数重载的目的是为了提高代码的可读性和复用性,让功能相似的函数可以使用相同的函数名。

注意:函数的返回值类型不同不能作为函数重载的依据,因为编译器在调用函数时不考虑返回值类型。

1. 参数类型不同

当两个函数的参数个数相同,但至少有一个对应位置的参数类型不同时,它们可以构成重载。

使用场景:实现相同逻辑但处理不同数据类型的函数,如计算两个数的和(int型、double型等)。

// 参数类型不同的重载函数
void func(int a) { // 参数为int类型
    std::cout << "int: " << a << std::endl;
}

void func(double a) { // 参数为double类型
    std::cout << "double: " << a << std::endl;
}

int main() {
    func(10);    // 调用func(int),输出int: 10(实参10是int类型,匹配第一个函数)
    func(10.5);  // 调用func(double),输出double: 10.5(实参10.5是double类型,匹配第二个函数)
    return 0;
}

2. 参数个数不同

当两个函数的参数类型相同,但参数个数不同时,它们可以构成重载。

使用场景:实现功能相似但参数数量不同的函数,如打印一个数、打印两个数等。

// 参数个数不同的重载函数
void func(int a) { // 1个参数
    std::cout << "一个参数: " << a << std::endl;
}

void func(int a, int b) { // 2个参数
    std::cout << "两个参数: " << a << ", " << b << std::endl;
}

int main() {
    func(10);     // 调用1个参数的func,输出一个参数: 10
    func(10, 20); // 调用2个参数的func,输出两个参数: 10, 20
    return 0;
}

3. 参数顺序不同

当两个函数的参数个数和类型都相同,但对应位置的参数类型顺序不同时,它们可以构成重载。

使用场景:参数顺序不同但功能相似的函数,如交换两个不同类型变量的值(虽然实际中交换函数通常参数类型相同,但此处仅为示例)。

// 参数顺序不同的重载函数
void func(int a, double b) { // 参数顺序:int, double
    std::cout << "int, double: " << a << ", " << b << std::endl;
}

void func(double a, int b) { // 参数顺序:double, int
    std::cout << "double, int: " << a << ", " << b << std::endl;
}

int main() {
    func(10, 20.5); // 实参类型:int, double,匹配第一个函数,输出int, double: 10, 20.5
    func(20.5, 10); // 实参类型:double, int,匹配第二个函数,输出double, int: 20.5, 10
    return 0;
}

4. 函数重载的原理

C++函数重载的实现依赖于编译器的名字修饰(Name Mangling) 机制。在编译阶段,编译器会根据函数的参数列表(参数类型、个数、顺序)对函数名进行修饰,生成一个唯一的内部名称。链接阶段, linker 会根据修饰后的名称查找函数的地址。

例如(不同编译器修饰规则不同,此处为简化示例):

  • void func(int) 可能被修饰为 _func_i(i表示int)。
  • void func(double) 可能被修饰为 _func_d(d表示double)。

这样,即使原函数名相同,修饰后的名称也不同,从而实现了重载。

5. 为什么不能根据函数的返回值进行函数重载?

编译器在解析函数调用时,主要根据函数名和参数列表来确定调用哪个函数,而不会考虑返回值类型。如果两个函数仅返回值类型不同,编译器无法在调用时区分它们,从而导致二义性。

示例

int func() { return 1; }    // 返回值类型为int
double func() { return 1.5; } // 返回值类型为double,与上面的函数仅返回值不同

int main() {
    func(); // 编译错误:二义性,编译器无法确定调用哪个func
    return 0;
}

原因:函数调用时可以忽略返回值(如func();),此时编译器无法通过返回值类型判断调用哪个函数。因此,C++不允许仅根据返回值类型来重载函数。

五、引用

引用(Reference)是C++中的一种复合类型,它是某个已存在变量的别名。引用本身不是一个独立的变量,它与被引用的变量共享同一块内存空间。通过引用可以直接访问和修改被引用变量的值。

1. 引用特性

1.1 引用在定义的时候必须初始化

引用在创建时必须明确指定它引用的是哪个变量,不能定义一个未初始化的引用。这是因为引用一旦定义,就必须与一个变量绑定,且在生命周期内不能更改绑定关系。

错误示例int& ra;(未初始化引用,编译错误)。

int main() {
    int a = 10; // 定义一个int变量a
    // int& ra; // 错误:引用必须初始化
    int& ra = a; // 正确:ra是a的引用(别名),必须在定义时初始化
    return 0;
}
1.2 一个变量可以有多个引用

一个变量可以被多个引用所引用,这些引用都是该变量的别名,它们指向同一块内存空间。修改任何一个引用的值,都会影响到原变量和其他引用。

int main() {
    int a = 10; // 原变量a
    int& ra = a; // ra是a的引用
    int& rra = a; // rra也是a的引用(一个变量可以有多个引用)
    std::cout << ra << ", " << rra << std::endl; // 输出10, 10(ra和rra都是a的别名)
    ra = 20; // 通过ra修改a的值
    std::cout << a << ", " << rra << std::endl; // 输出20, 20(a和rra的值都变为20)
    return 0;
}
1.3 一旦成为了某个变量的引用,引用将一直效忠这个变量

引用在初始化时与某个变量绑定后,在其生命周期内就不能再绑定到其他变量。这一点与指针不同,指针可以随时指向其他变量。

注意:引用的赋值操作是修改被引用变量的值,而不是更改引用的绑定关系。

int main() {
    int a = 10, b = 20;
    int& ra = a; // ra初始化为a的引用(绑定到a)
    ra = b; // 这里是将b的值(20)赋给ra所引用的变量a,而不是让ra改为引用b
    std::cout << a << std::endl; // 输出20(a的值被修改为20)
    std::cout << ra << std::endl; // 输出20(ra仍然引用a)
    return 0;
}

2. 常引用(指针和引用的赋值,权限可以缩小不能扩大)

常引用(const Reference)是指向const对象的引用,其语法为const 类型& 引用名 = 被引用对象;。常引用的特点是:不能通过常引用来修改被引用对象的值。在C++中,引用的权限可以缩小(如非const变量绑定到const引用),但不能扩大(如const变量绑定到非const引用)。

2.1 对常量的引用

如果被引用的对象是一个常量(如const int a = 10;),则必须使用常引用来引用它。非const引用不能引用常量,因为这会扩大权限(允许通过引用修改常量,而常量是不可修改的)。

int main() {
    const int a = 10; // a是const常量,值不可修改
    // int& ra = a; // 错误:权限扩大(非const引用指向const变量,可能通过ra修改a,违反a的const属性)
    const int& ra = a; // 正确:权限一致(const引用指向const变量)
    // ra = 20; // 错误:常引用不能修改被引用对象的值
    return 0;
}
2.2 对const常量的引用

与对常量的引用类似,对const变量的引用也必须是常引用。这确保了不能通过引用修改const变量的值,维护了const的语义。

int main() {
    const int a = 10; // const变量a
    const int& ra = a; // 常引用ra引用a
    std::cout << ra << std::endl; // 输出10(可以通过常引用读取a的值)
    return 0;
}
2.3 隐式、强制类型转换的引用

当一个变量进行类型转换(隐式或强制)时,会产生一个临时变量来存储转换后的值。临时变量具有const属性,因此只能用常引用来引用它,非const引用不能引用临时变量(会扩大权限)。

示例int a = 10; double b = a;中,a(int)转换为double时会产生一个临时的double变量,然后赋值给b

int main() {
    int a = 10; // int类型变量a
    // double& rd = a; // 错误:a转换为double时产生临时变量(const属性),非const引用不能引用临时变量
    const double& rd = a; // 正确:常引用可以引用临时变量(权限缩小)
    std::cout << rd << std::endl; // 输出10.0(rd引用的是a转换后的临时double变量)
    return 0;
}
2.4 传参时的权限问题

函数传参时,如果实参是普通变量(非const),形参可以是const引用(权限缩小);但如果实参是const变量,形参不能是非const引用(权限扩大)。

使用场景:函数形参为const引用时,可以接收const和非const实参,且避免了值传递的拷贝开销,同时保证实参不被修改。

void func(const int& a) { // 形参为const引用
    // a = 20; // 错误:const引用不能修改实参的值
}

int main() {
    int a = 10; // 非const实参
    const int b = 20; // const实参
    func(a); // 正确:非const实参传递给const引用(权限缩小)
    func(b); // 正确:const实参传递给const引用(权限一致)
    return 0;
}

3. 引用的使用场景

3.1 引用做参数

引用作为函数参数时,形参是实参的别名,函数内部对形参的修改会直接影响到实参。这种方式避免了值传递时的拷贝开销(尤其是对于大型对象),同时可以方便地修改实参的值。

引用做参数的优势

  • 效率高:无需拷贝实参,对于大型对象(如自定义类、结构体),可以显著提升性能。
  • 直接修改实参:通过引用可以直接修改函数外部的变量,无需使用指针。
  • 语法简洁:相比指针,引用的使用更直观,无需解引用操作符*
// 引用做参数:交换两个整数的值
void swap(int& a, int& b) { // a和b是实参的引用
    int temp = a; // temp是a的值的拷贝
    a = b; // 修改a(即修改实参)
    b = temp; // 修改b(即修改实参)
}

int main() {
    int x = 10, y = 20;
    std::cout << "交换前:x=" << x << ", y=" << y << std::endl; // 输出交换前:x=10, y=20
    swap(x, y); // 传递x和y的引用
    std::cout << "交换后:x=" << x << ", y=" << y << std::endl; // 输出交换后:x=20, y=10
    return 0;
}
3.2 引用做返回值

引用也可以作为函数的返回值,此时函数返回的是某个变量的引用(别名)。使用引用返回可以避免返回值的拷贝开销,还可以将函数调用作为左值使用(即可以对返回的引用赋值)。

注意:不能返回函数内部局部变量的引用,因为局部变量在函数调用结束后会被销毁,返回的引用将成为悬垂引用(指向已释放内存),访问时会导致未定义行为。

引用返回相较于传值返回的优势

  • 减少拷贝:对于大型对象,引用返回避免了返回值的拷贝,提高效率。
  • 支持链式操作:可以像obj.func1().func2()这样进行链式调用(如果func1返回引用)。
  • 可作为左值:允许对返回的引用直接赋值(如func() = 10;)。
// 引用做返回值:返回变量a的引用
int& func(int& a) { // 形参a是实参的引用
    a++; // 修改a的值
    return a; // 返回a的引用(注意:a不是局部变量)
}

int main() {
    int x = 10;
    func(x) = 20; // 引用返回可以作为左值,将20赋给func(x)返回的引用(即x)
    std::cout << x << std::endl; // 输出20(x的值被修改为20)
    return 0;
}
3.3 引用和指针的区别

引用和指针在功能上有相似之处(都可以间接访问其他变量),但它们在语法、语义和使用场景上有显著区别:

区别 引用 指针
定义 引用是变量的别名,定义时必须初始化 指针是存储变量地址的变量,可初始化为nullptr
空值 没有空引用(必须绑定到一个有效变量) 有空指针(nullptr),表示不指向任何变量
绑定关系 一旦初始化,不能更改绑定的变量 可以随时改变指向的变量地址
大小 引用的大小与被引用变量的大小相同 指针的大小通常为4字节(32位系统)或8字节(64位系统)
多级 没有多级引用(如int&&是右值引用,非二级引用) 有多级指针(如int**,指向指针的指针)
安全性 更安全,不存在野引用(合理使用时) 存在野指针风险(指向已释放内存)
语法 使用时无需解引用(直接使用) 使用时需要解引用(*运算符)

六、inline关键字

1. inline的概念

inline(内联)是C++中的一个关键字,用于建议编译器将函数调用处直接替换为函数体,从而消除函数调用的开销(如压栈、跳转、返回等操作)。内联函数通常用于函数体较小、被频繁调用的场景(如简单的数学运算函数)。

注意inline是编译器的建议,而非强制命令。编译器会根据函数体大小、调用频率等因素决定是否内联(如递归函数、复杂函数通常不会被内联)。

2. inline修饰的函数的特点

  • 消除调用开销:内联函数在调用处被展开,避免了函数调用的压栈、跳转和返回等操作,提高程序运行效率。
  • 可能增加代码体积:如果内联函数体较大或被频繁调用,多次展开会导致可执行文件体积增大(代码膨胀),反而可能降低效率(影响缓存命中率)。
  • 建议性:编译器可以忽略inline关键字(如函数体包含循环、递归时)。
  • 定义通常在头文件中:内联函数的定义通常放在头文件中,而不是单独的源文件。这是因为编译器需要在编译时看到函数体才能进行内联展开,如果内联函数的定义在源文件中,其他文件包含头文件时只能看到声明,无法内联。
  • 不能声明和定义分离:如果内联函数的声明和定义分离在不同文件中,会导致链接错误(因为编译器可能在每个包含头文件的源文件中生成内联函数的副本,链接时发现多个定义)。
// 内联函数定义(通常放在头文件中)
inline int add(int a, int b) { // 使用inline修饰
    return a + b; // 函数体较小,适合内联
}

int main() {
    int sum = add(1, 2); // 编译器可能将此处替换为sum = 1 + 2(内联展开)
    std::cout << sum << std::endl; // 输出3
    return 0;
}

3. 宏的优缺点和替代

在C++中,inline函数常被用来替代C语言中的宏(Macro)。宏是通过预处理器实现的文本替换,虽然可以减少函数调用开销,但存在诸多缺点。

宏的优点

  • 代码复用:宏可以实现简单的代码复用,减少重复编写。
  • 减少调用开销:宏在预处理阶段展开,避免了函数调用的开销(与内联类似)。

宏的缺点

  • 没有类型检查:宏在预处理时进行文本替换,不进行类型检查,可能导致类型错误。
  • 可能产生副作用:宏展开时可能会对表达式进行多次求值,导致意外结果。
    示例#define ADD(a,b) a+b,调用ADD(1,2)*3会被展开为1+2*3=7,而非预期的(1+2)*3=9
  • 难以调试:宏在预处理阶段被替换,调试时无法看到宏的调用,只能看到展开后的代码。
  • 语法复杂:宏定义中需要注意括号的使用,否则容易出现优先级问题。

替代方案

  • 内联函数(inline:替代带参数的宏,提供类型安全和避免副作用,同时保留宏的效率优势。
  • const常量:替代#define定义的常量(如#define PI 3.14可替换为const double PI = 3.14;),const常量有类型、有作用域,更安全。

七、auto的用法

auto是C++11引入的关键字,用于自动推导变量的类型。编译器会根据变量的初始化值自动推断出变量的类型,从而简化代码书写,尤其是在类型名较长或复杂的情况下(如STL容器的迭代器类型)。

使用规则

  • auto变量必须在定义时初始化(编译器需要根据初始化值推导类型)。
  • auto不能用于函数参数的类型推导(如void func(auto a) { ... }是不允许的)。
  • auto不能用于数组类型的推导(如auto arr = {1,2,3};推导的是std::initializer_list<int>,而非数组)。
  • 可以与constvolatile&*等结合使用,推导出复合类型。
int main() {
    int a = 10;
    auto b = a; // 编译器根据a的类型(int)推导出b的类型为int
    auto c = 10.5; // 根据10.5推导出c的类型为double
    auto d = "hello"; // 根据字符串字面量推导出d的类型为const char*
    auto e = &a; // 根据&a(int*)推导出e的类型为int*
    
    std::cout << typeid(b).name() << std::endl; // 输出int(不同编译器输出格式可能不同)
    std::cout << typeid(c).name() << std::endl; // 输出double
    std::cout << typeid(d).name() << std::endl; // 输出const char*
    return 0;
}

应用场景

  • 简化复杂类型声明:如std::vector<int>::iterator it = vec.begin();可简化为auto it = vec.begin();
  • 泛型编程:与模板结合使用,处理多种类型的变量。
  • 避免类型拼写错误:减少手动书写长类型名时的拼写错误。

八、范围for

范围for(Range-based for Loop)是C++11引入的一种新的循环语法,用于遍历数组或容器(如std::vectorstd::string等)中的所有元素。它的语法简洁,可读性高,避免了传统for循环中繁琐的索引或迭代器操作。

语法格式

for (元素类型 元素变量 : 容器/数组) {
    // 循环体,处理元素变量
}

说明

  • 元素类型:可以是容器中元素的类型,也可以用auto让编译器自动推导。
  • 元素变量:循环中当前元素的副本或引用。如果需要修改元素的值,应使用引用(auto&);如果只需读取,可使用const auto&(避免拷贝,提高效率)。
  • 容器/数组:要遍历的对象,必须是可迭代的(如数组、STL容器等),且能确定范围(有明确的开始和结束位置)。
int main() {
    int arr[] = {1, 2, 3, 4, 5}; // 数组
    // 1. 遍历数组元素(元素变量是副本,修改不影响原数组)
    std::cout << "遍历输出数组元素:";
    for (int e : arr) { // e是数组元素的拷贝
        std::cout << e << " ";
    }
    std::cout << std::endl;
    
    // 2. 引用遍历(修改元素值)
    for (int& e : arr) { // e是数组元素的引用
        e *= 2; // 将每个元素乘以2
    }
    
    // 3. const引用遍历(只读,避免拷贝)
    std::cout << "修改后的数组元素:";
    for (const int& e : arr) { // const引用,不能修改元素
        std::cout << e << " ";
    }
    std::cout << std::endl;
    return 0;
}

输出结果

遍历输出数组元素:1 2 3 4 5 
修改后的数组元素:2 4 6 8 10 

与传统for循环的对比

  • 传统for循环:需要手动管理索引或迭代器(如for (int i = 0; i < 5; i++) { ... }),容易出错(如越界)。
  • 范围for:语法简洁,无需关心容器大小和索引,降低出错风险,提高代码可读性。

九、关键字nullptr

nullptr是C++11引入的关键字,用于表示空指针常量,专门用来初始化空指针。在C++11之前,通常使用NULL宏来表示空指针,但NULL存在一些缺陷,nullptr的引入就是为了替代NULL

NULL的缺陷
NULL在C++中通常被定义为#define NULL 0(或#define NULL (void*)0,但在C++中会被隐式转换为整数0)。这导致NULL既可以表示空指针,也可以表示整数0,可能引发二义性。

nullptr的优势

  • 类型安全nullptr的类型是nullptr_t,它可以隐式转换为任何指针类型(如int*char*等),但不能转换为整数类型。
  • 避免二义性:在函数重载场景中,nullptr能明确匹配指针类型的重载函数,而NULL可能匹配整数类型的重载函数。
// 函数重载:一个接收int,一个接收int*
void func(int a) {
    std::cout << "int: " << a << std::endl;
}

void func(int* p) {
    std::cout << "int*: " << p << std::endl;
}

int main() {
    // func(NULL); // 二义性错误:编译器无法确定调用func(int)还是func(int*)(因为NULL被定义为0)
    func(nullptr); // 正确:调用func(int*),因为nullptr是指针类型
    return 0;
}

输出结果

int*: 0

最佳实践:在C++中,始终使用nullptr表示空指针,而不是NULL或0,以避免二义性和提高代码可读性。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐