【C++】入门预备知识

目录

前言 

1、域

2、命名空间

3、输入输出

4、缺省参数

5、函数重载

6、引用

7、auto 关键字 

8、基于范围的 for 循环

9、内联函数

10、nullptr


前言 

C++的主要优点
C++作为一门已有40多年历史的编程语言,能够在快速变化的计算机领域长期保持重要地位,主要归功于以下几个核心优势:

1. 高性能与高效性

  • 接近硬件的性能:C++代码编译后效率极高,几乎可以达到手写汇编的性能
  • 零成本抽象:高级特性(如模板、RAII)几乎不引入运行时开销
  • 精细的内存控制:允许手动内存管理,避免垃圾回收的不可预测停顿
  • 内联和优化:编译器可以进行深度优化,生成极其高效的机器码

2. 多范式支持

  • 面向对象编程:支持类、继承、多态等OOP特性
  • 泛型编程:通过模板实现高度灵活的通用代码
  • 过程式编程:保留C风格的过程化编程能力
  • 函数式编程:C++11后引入lambda、函数对象等函数式特性
  • 元编程:通过模板元编程在编译期执行复杂计算

3. 广泛的应用领域

  • 系统/操作系统开发:Windows/Linux内核、驱动等
  • 游戏开发:Unreal、Unity等主流游戏引擎
  • 高频交易:金融行业的低延迟系统
  • 嵌入式系统:资源受限的物联网设备
  • 科学计算:高性能数值计算和模拟
  • 图形处理:3D渲染、计算机视觉等

4. 精细的资源控制

  • 确定性的资源管理:通过RAII模式保证资源及时释放
  • 直接内存访问:指针和引用提供底层内存操作能力
  • 自定义内存管理:可重载new/delete,实现特殊内存分配策略
  • 位级操作:可直接操作硬件寄存器和特定内存位置

5. 丰富的标准库和生态系统

  • STL(标准模板库):提供强大的容器、算法和迭代器
  • Boost等高质量库:扩展了标准库的功能
  • 跨平台支持:可在几乎所有硬件平台和操作系统上运行
  • 成熟的工具链:强大的编译器(如GCC、Clang、MSVC)和调试工具

6. 向后兼容与稳定性

  • 长期兼容性:几十年积累的代码仍可编译运行
  • 标准化发展:由ISO委员会规范,发展稳健可控
  • 渐进式改进:每三年发布新标准,平衡创新与稳定

7. 社区与行业支持

  • 庞大的开发者社区:全球数百万C++开发者
  • 行业标准地位:许多关键系统依赖C++实现
  • 持续现代化:C++11/14/17/20/23不断引入现代特性
  • 教育价值:学习C++能深入理解计算机原理

C++值得吐槽的痛点
尽管C++功能强大,但它确实存在不少让人抓狂的设计缺陷和历史包袱。以下是被开发者广泛吐槽的几个方面:

1. 语法复杂且不一致

  • 操作符重载滥用:<< 既表示位移又用于输出,+ 可能执行任意操作
  • 最恼人的解析(Most Vexing Parse):TimeKeeper time_keeper(Timer()); 实际声明函数而非对象
  • 初始化方式过多:C风格、构造函数、列表初始化、统一初始化等至少5种方式
  • 模板错误信息灾难:简单的模板错误能产生上百行难以理解的编译器输出

2. 历史包袱沉重

  • 兼容C带来的问题:必须保留C的原始指针、宏、隐式转换等危险特性
  • 头文件系统过时:#include 是简单的文本替换,导致编译缓慢
  • 预处理器滥用:宏污染命名空间,难以调试(C++20模块才开始改善)

3. 内存安全问题

  • 悬垂指针/引用:访问已释放内存是常见错误
  • 手动内存管理:忘记delete导致内存泄漏,或重复delete导致崩溃
  • 未定义行为(UB)陷阱:数组越界、空指针解引用等行为完全不可预测

4. 编译与构建问题

  • 编译速度慢:模板实例化使编译时间随项目规模指数增长
  • 二进制兼容性问题:不同编译器甚至同一编译器的不同版本可能不兼容
  • 构建系统复杂:CMake语法反人类,但又是事实标准

5. 学习曲线陡峭

  • 特性膨胀:C++11/14/17/20/23不断增加新特性,语言规范已超2000页
  • 多重范式混乱:同一项目可能混杂面向对象、泛型、函数式等多种风格
  • 专家级陷阱:即使经验丰富的开发者也会踩中UB或模板元编程的坑

6. 现代化进程的尴尬

  • 新特性采用缓慢:许多项目仍停留在C++98/03,因旧代码迁移成本高
  • 半吊子改进:比如std::string_view解决了一些问题,但依然不是默认字符串类型
  • 模块化姗姗来迟:C++20才引入模块,生态迁移需要多年

7. 工具链痛点

  • 调试困难:模板展开后的代码、优化后的二进制难以调试
  • 包管理落后:没有官方包管理器(Conan/vcpkg等第三方方案各有缺陷)
  • IDE支持有限:由于语言复杂性,代码补全和重构不如Java/C#流畅

正如Bjarne Stroustrup所说:"C++的设计目标就是让你能轻松射击自己脚部,同时让高手能写出优雅高效的代码。"

 1、域

域有类域、命名空间域、局部域、全局域等。

// 全局域
int a = 0;

// 命名空间域
namespace yu
{
    int a = 1;
}

int main()
{
    // 局部域
    int a = 2;

    return 0;
}

main 函数中如果要访问 a 变量,默认先在局部域访问,如果局部域没有定义,就在全局域访问,如果全局域没有定义,并且没有展开命名空间域或指定访问命名空间域,编译器不会在命名空间域访问,而是会报错。 

:: 限定域访问符

:: 的作用是告诉编译器优先在哪里访问,使用的基本形式:

域  :: 变量名

如果 :: 前面什么都没有或只有空格,表示在全局域访问,如果是命名空间名,表示在命名空间内访问。

2、命名空间

#include <stdio.h>
#include <stdlib.h>

int rand = 10;
int main()
{
    printf("%d", rand);
    return 0;
}

以上程序会报错:“rand”: 重定义;以前的定义是“函数”,原因是 stdlib.h 文件中定义了 rand 函数,我们又定义了一个全局变量也叫 rand,这就产生了命名冲突的问题。C++ 采用“命名空间”来解决此类问题。

举个例子来说明命名空间如何使用:

#include <stdio.h>
#include <stdlib.h>

namespace variable
{
    int a = 10;
}

namespace bit
{
    // 命名空间中可以定义变量/函数/类型
    int rand = 10;

    int Add(int left, int right)
    {
        return left + right;
    }

    struct Node
    {
        struct Node* next; 
        int val;
    };
}

// 命名空间可以嵌套
// test.cpp 
namespace N1
{
    int a;
    int b;
    int a1;
    int Add(int left, int right)
    {
        return left + right;
    }
    // N1 中嵌套 N2
    namespace N2
    {
        int c; 
        int d; 
        int a1;//与命名空间 N1 的 a1 是可以同时存在的
        int Sub(int left, int right)
        {
            return left - right;
        }
    }
}
//3. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
// ps:一个工程中的test.h和上面test.cpp中两个N1会被合并成一个

namespace N1
{
    int Mul(int left, int right)
    {
        return left * right;
    }
}

//全部展开命名空间
using namespace variable;

//部分展开命名空间
using N1::b;

int main()
{
    printf("%d", a);

    //访问嵌套的命名空间里的变量
    printf("%d",N1::N2::a);

    //访问命名空间里的函数
    printf("%d",N1::Add(1,1));

    return 0;
}

定义了命名空间 variable ,并展开该命名空间。using namespace 的作用是将命名空间包含在全局域。如果全局域也有一个变量叫 a,编译器会报错说 a 不明确,所以不能随便轻易使用 using 关键字。但可以使用 using 关键字将命名空间部分展开,比如说 using std::cout; 

运用命名空间和限定域访问符就可以解决命名冲突的问题:

#include <stdio.h>
#include <stdlib.h>

namespace variable
{
    int rand = 10;
}

int main()
{
    // 访问自己定义的 rand
    printf("%d\n", variable::rand);

    // 访问库定义的 rand
    printf("%p\n", rand);
    
    return 0;
}

std命名空间的使用惯例:
std是C++标准库的命名空间,如何展开std使用更合理呢?
1. 在日常练习中,建议直接using namespace std即可,这样就很方便。
2. using namespace std展开,标准库就全部暴露出来了,如果我们定义跟库重名的类型/对象/函数,就存在冲突问题。该问题在日常练习中很少出现,但是项目开发中代码较多、规模大,就很容易出现。所以建议在项目开发中使用像std :: cout这样展开常用的库对象/类型等方式。

3、输入输出

需要包含头文件:include <iostream>,并且要展开 cout 、 endl、cin 等。

<< 流插入运算符,>> 流提取运算符

输出

输出字符串:

cout << "Hello world" << endl;

在屏幕上就打印了 ”Hello world“,cout 可以理解为终端(黑框框), << 可以理解为”流向“,即        ”Hello world“ 流向了终端(黑框框),endl 是 end line,等价于 ‘\n' 。

输出变量:

int x = 1;
double y = 1.1;
cout << x << y << endl;

相对于 printf 来说这种方式可以自动识别变量的类型,用相应的方式来输出变量。如果想控制输出变量的小数位数,可以使用 c 语言的 printf ,而不使用 c++ 的方式,因为 c 语言的方式更方便。

输入

int a = 0;
cin >> a;

一次输入多个变量:

int a , b , c;

cin >> a >> b >> c;

在终端中输入 1 2 3 再按回车,a,b,c就被赋值为 1,2,3. 

c 语言的 printf 和 scanf 比 c++ 的效率要更高。

getline 函数 

有时候向string对象输入字符时,不想以空格为分割(仍以‘\n' 为输入结束),就可以使用 getline 函数:

#include<iostream>
#include<string>
using namespace std;

int main()
{
	string a;
	getline(cin, a);
	return 0;
}

在终端输入 “a b c d ", a 的内容就是 a b c d。

istream 的 get 成员函数

istream 类有一个成员函数叫 get ,它可以获取包括空格和换行符的输入:

#include<iostream>
#include<string>
using namespace std;

int main()
{
	char ch;
	ch = cin.get();
	return 0;
}

但这样就无法结束输入了,通常结合循环条件来结束输入:

#include<iostream>
#include<string>
using namespace std;

int main()
{
	char ch;
	string s;
	ch = cin.get();
	while (ch != ' ' && ch != '\n')
	{
		s += ch;
		ch = cin.get();
	}
	return 0;
}

 如果循环条件是 while(ch != '\n') ,那就是 getline 的模拟实现了。

4、缺省参数

缺省参数(默认参数)是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。

void Func(int a = 0)
{
    cout << a << endl;
}

int main()
{
    // 没有传参时,使用参数的默认值
    Func(); 
    
    // 传参时,使用指定的实参
    Func(10);

    return 0;
}

缺省参数的分类:

全缺省参数

void Func(int a = 10, int b = 20, int c = 30)
{
    cout << "a = " << a << endl; 
    cout << "b = " << b << endl; 
    cout << "c = " <<<<< endl;
}
int main()
{
    Func(); 
    Func(1);     // 1 传给了 a
    Func(1,2);   // 1 传给了 a,2 传给了 b
    Func(1,2,3);

    //Func(1,,3) error

    return 0;
}

半缺省参数

void Func(int a, int b = 10, int c = 20)
{
    cout << "a = " << a << endl; 
    cout << "b = " << b << endl; 
    cout << "c = " << c << engl << endl;
}

注意:
1. 半缺省参数必须从右往左依次来给出,不能间隔着给
2. 缺省参数不能在函数声明和定义中同时出现,防止函数声明和定义中的缺省参数不一致。

缺省参数只能在函数声明中出现,因为预处理阶段包含头文件时将函数声明拷贝替换的是函数声明,如果缺省参数在函数定义出现,那么编译器在检查函数调用时的参数是否合法时会报错

缺省参数的应用 

在初始化栈的时候,有时候我们已知数据元素的个数,那么我们在初始化时候能不能开辟相应的大小呢?是可以的,只需要给初始化函数多设置一个形参就行了,但这个形参又不是必须接收实参的,比如不知道数据元素的个数,这时我们不可以随便开辟一定空间,因为可能开小了要扩容,开大了要浪费空间,这时就可以使用缺省参数:

void StackInit(struct Stack* pst, int defaultCapacity = 4)
{
    pst->a = (int*)malloc(sizeof(int) * defaultCapacity); 
    if (pst->a == NULL)
    {
        perror("malloc fail");
        return;
    }
    
    pst->top = 0;
    pst->capacity = defaultCapacity;
}

int main()
{
    struct Stack st1; 
  
    // 插入100个数据
    StackInit(&st1, 100);
   
    // 不知道要插入多少数据
    struct Stack st2; 
    StackInit(&st2);
} 

defaultCapacity 就是一个缺省参数

5、函数重载

函数重载:是函数的一种特殊情况,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 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;
}

// 4、参数的权限不同
void f(int a)
{
    cout << "f(int a)" << endl;
}

void f(const int a)
{
    cout << "f(int a)" << endl;
}

// 5、函数的权限不同
void f(int a)
{
    cout << "f(int a)" << endl;
}

void f(int a) const
{
    cout << "f(int a)" << endl;
}

// 6、参数的引用类型不同
void f(int& a)
{
    cout << "f(int a)" << endl;
}

void f(int&& a) const
{
    cout << "f(int a)" << endl;
}

会发生歧义的函数重载:

void f()
{
    cout << "f()" << endl;
}

void f(int a = 0)
{
    cout << "f(int a)" << endl;
}

按照函数重载的定义,以上函数构成重载,但如果无参调用会有歧义。

c++ 是如何支持函数重载的呢?

// Stack.h

typedf StackType int;

struct Stack
{
    StackType* a;
    int top;
    int capacity;
};

void StackInit(struct Stack* pst, int defaultCapacity = 4)
{
    pst->a = (int*)malloc(sizeof(int) * defaultCapacity); 
    if (pst->a == NULL)
    {
        perror("malloc fail");
        return;
    }
    
    pst->top = 0;
    pst->capacity = defaultCapacity;
}

void StackPush(struct Stack* pst,int a);
void StackPush(struct Stack* pst,double a);

// Stack.cpp

#include "Stack.h"

void StackPush(struct Stack* pst,int a)
{
    printf("StackPush(struct Stack* pst,int a)");
}

void StackPush(struct Stack* pst,double a)
{
    printf("StackPush(struct Stack* pst,double a)");
}

// Test.c

#include "Stack.h"

int main
{   
    Stack s;
    StackInit(&s);
    StackPUsh(&s,1);
    StackPUsh(&s,1.1);

    return 0;
}

编译链接过程回顾:

在 call 指令后有函数的地址 07B1492h ,执行 call 指令后会跳转到 jmp 指令,jmp 指令会跳转到函数。那么 StackPush 函数的地址是在什么时候生成的呢?

在 Test.i 经过编译后生成 Test.s 后,函数的地址并没有在 Test.s 中生成,而是在 Stack.s 中生成。因为 Test.s 包含的头文件里是函数的声明而不是定义,在 Stack.s 里才有函数的定义。Test.i 经过编译后生成 Test.s 的过程中,编译器只是检查调用函数时的实参与函数声明(函数声明是预处理阶段拷贝过来的,这也说明了函数中的缺省参数要在声明中定义)中的形参是否匹配,函数的地址编译器暂时不管,而函数的地址是在链接时确定。如果函数的定义也在 Test.i ,那么在编译时直接生成该函数地址。

在 c 语言中,直接靠函数名来区分函数,但在 c++ 中,生成的两个函数名不同:

C++ 函数名修饰(Name Mangling)规则

函数名修饰(Name Mangling)是 C++ 编译器在编译过程中对函数名进行编码的一种机制,目的是为了支持 函数重载命名空间模板 等 C++ 特性。由于 C++ 允许函数重载(相同函数名,不同参数列表),编译器需要生成唯一的符号名,以便链接器正确识别和链接。


1. 为什么需要 Name Mangling?

在 C 语言中,函数名在符号表中就是原始名称(如 printf),但 C++ 由于支持:

  • 函数重载void foo(int) 和 void foo(double)

  • 命名空间ns::func

  • 类成员函数MyClass::method

  • 模板函数std::vector<int>::push_back

编译器必须对函数名进行编码,使其唯一,例如:

  • void foo(int) → _Z3fooi

  • void foo(double) → _Z3food


2. 不同编译器的修饰规则

C++ 标准没有规定统一的 Name Mangling 规则,不同编译器采用不同的方案:

(1) GCC/Clang (Itanium ABI)

采用 Itanium C++ ABI 规则(Linux/macOS 默认):

  • 格式:_Z + 函数名长度 + 函数名 + 参数类型编码

  • 示例:

    int foo(int);           // _Z3fooi
    float bar(double, int); // _Z3bardf
    namespace ns {
        void func();        // _ZN2ns4funcEv
    }
    class MyClass {
        void method();      // _ZN7MyClass6methodEv
    };

(2) MSVC (Microsoft ABI)

Windows 平台使用不同的规则:

  • 格式:? + 函数名 + 参数类型 + 返回类型 + 其他修饰

  • 示例:

    int foo(int);           // ?foo@@YAHH@Z
    float bar(double, int); // ?bar@@YAMHN@Z
    namespace ns {
        void func();        // ?func@ns@@YAXXZ
    }
    class MyClass {
        void method();      // ?method@MyClass@@QEAAXXZ
    };

6、引用

引用不是新定义一个变量,而是给已存在的变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。

类型& 引用变量名(对象名) = 引用实体;

1. 引用在定义时必须初始化
2. 一个变量可以有多个引用
3. 引用一旦引用一个实体,再不能引用其他实体

void TestRef()
{
    int a = 10; 
    int& ra = a;// <==== 定义引用类型

    printf("%p\n", &a); 
    printf("%p\n", &ra);
}

注意:引用类型必须和引用实体是同种类型的

引用这种语法优化了指针的复杂性:

1、Swap 函数不再使用指针:Swap 函数现在可以直接传变量而不是变量的地址。

void Swap(int& a, int& b)
{
    int tmp = a; 
    a = b; 
    b = tmp;
}

// 对比:使用指针

void Swap(int* a, int* b)
{
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

2、交换一级指针的指向不再使用二级指针:

void Swap(int*& a, int*& b)
{
    int* tmp = a; 
    a = b; 
    b = tmp;
}

// 对比:使用二级指针

void Swap(int** a, int** b)
{
    int* tmp = *a;
    *a = *b;
    *b = tmp;
}

引用的使用场景

1. 做输出型参数(形参的改变影响到实参的值)

void Swap(int& left, int& right)
{
    int temp = left; 
    left = right; 
    right = temp;
}

2. 做返回值(传值返回与传引用返回)

int& Count ()
{
    static int n = 0;
    n++;
    // ... 
    return n;
}

函数返回值时,通常将返回的值先存储在寄存器中,等退出函数时,函数栈帧被销毁,此时待返回的值在寄存器中。在返回到的函数栈帧中,寄存器的值赋值给调用函数的表达式,作为该表达式的值。寄存器的大小通常只有 4 字节。

如果返回的值是静态的变量,那么该返回值仍会被存储到寄存器里去吗?

答案是会,因为这样可以简化编译器的设计,编译器不想考虑多种情况,它想作统一处理。

那怎么不使用寄存器(临时变量)呢?

方法当然是使用引用作为返回值。

不使用寄存器(临时变量)有什么价值?

如果函数的参数是大对象(占用空间很多),这样可以减少拷贝,提升效率。

如果以上代码的 n 不是静态的:

int& Count()
{
    int n = 0; 
    n++;
    return n;
}

int main()
{
    int ret = Count(); 
    cout << ret << endl;
    return 0;
}

这里打印 ret 的值是不确定的

如果 Count 函数结束,栈帧销毁,没有清理栈帧,那么ret的结果侥幸是正确的。

如果 Count 函数结束,栈帧销毁,清理栈帧,那么ret的结果是随机值。

int& Count()
{
    int n = 0; 
    n++;  
    return n;
}

int main()
{
    int& ret = Count(); 
    cout << ret << endl; 

    Count(); 
    cout << ret << endl;
    return 0;
}

总结:
1、基本任何场景都可以用引用传参。

2、谨慎用引用做返回值。出了函数作用域,对象不在,就不能用引用返回,还在就可以用引用返回。

引用作返回值的两大功能:

1、 减少拷贝,提升效率

2、 获取、修改返回值

获取、修改返回值:

int& SLAt(SeqList& ps, int pos)
{
    assert(pos < 100 && pos >= 0);
    return s.a[pos];
}

以上代码可以获取顺序表第 pos 位置的值,并且可以对 pos 位置的值在函数外直接修改

//对下标为 0 的位置赋值为 1
SLAt(s, 0) = 1;

//对下标为 0 的位置增加 5
SLAt(s, 0) += 5;

如果不使用引用,则要先获取第 pos 位置的值(SLGet 函数),再对 pos 位置进行修改 SLModify 函数)

常引用

在引用过程中,权限可以平移或缩小,但不能放大。

const int a = 0;
int& b = a;
// 不可以
// 引用过程中,权限不能放大

int x = 0; 
int& y = x; 
// 可以
// 引用过程中,权限可以平移或者缩小

const int& z = x; //缩小 z 作为引用的权限
++x;//可以 ++x,此时 z 的值增加 1,但不能 ++z

const int& a = 10;//给常量取别名

double a = 1.1
int b = a; // 可以,隐式类型转换

// 隐式类型转换会产生临时变量,临时变量具有常性,要用常引用
int& b = a; // 不可以
const int& b = a; // 可以 

不管是隐式类型转换还是显式类型转换都会产生临时变量,临时变量具有常性,要传递给常引用的函数参数(const 类型名&) 

上图解释了为什么 double 类型可以取 const int& 类型的别名,因为临时变量具有常性。

int func1()
{
    static int x = 0;
    return x;
}

int& func2()
{
    static int x = 0; 
    return x;
}

 func1 函数的返回值不能用 int& 类型接收,因为函数的返回值具有常性。要用 const int& 类型接收

func2 函数的返回值可以用 int& 类型接收(权限平移),也可以用 const int& 类型接收(权限缩小) 

指针的引用

在某个类中有以下变量的声明:

float* &c;

c 是 一个指向 float 类型的指针的引用。

引用与指针的区别

 从底层汇编指令实现的角度看,引用是类似指针的方式实现的

引用和指针的不同点:
1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
2. 引用在定义时必须初始化,指针没有要求
3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
4. 没有NULL引用,但有NULL指针
5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
7. 有多级指针,但是没有多级引用
8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
9. 引用比指针使用起来相对更安全

7、auto 关键字 

auto 可以根据右边的的表达式自动推导要创建的变量的类型。当变量的类型很长时,可以用 auto 简化。

使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。

auto的使用细则
1. auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&

int main ()
{
    int x = 10; 
    auto a = &x; 
    auto* b = &x; 
    auto& c = x:

    return 0;
}

2. 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。

void TestAuto ()
{
    auto a = 1, b = 2;
    auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}

auto 不能推导的场景

1. auto不能作为函数的参数

// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)

2. auto不能直接用来声明数组

void TestAuto
{
    int a[] = {1,2,3}; 
    auto b[] = {4, 5, 6};
}

8、基于范围的 for 循环

基本用法:

for (auto e : arr)
{
    cout << e << " ";
}
cout << endl;

依次取 arr 的元素赋值给 e,自动判断结束。

由于 e 是 arr 元素的拷贝,所以 e 的改变不影响 arr,如果要用范围 for 来修改 arr 数组,e 必须是arr 元素的引用:

// 修改数据
for (auto& e : arr)
{
    e *= 2;
}

如果 arr 中的每个元素所占空间比较大,为了提升效率,也可以使用引用。 

范围for的使用条件
for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
注意:以下代码就有问题,因为for的范围不确定

void TestFor (int array[])
{
    for (auto& e : array)
        cout << e << endl;
}

范围 for 的底层实现原理是迭代器 

9、内联函数

有些函数需要被频繁的调用,调用函数时建立函数栈帧的时间开销很大。

#include<iostream>
using namespace std;

int Add(int x, int y)
{
    return (x + y) * 10;
}

int main()
{
    for (int i = 0; i < 10000; i++)
    {
        cout << Add(i, i + 1) << endl;
    }
    
    return 0;
}

上面的 Add 函数要被调用 10000 次,就要建立 10000 次栈帧。

解决方法1:宏函数

#define Add(X,Y) (((X) + (Y)) * 10)

解决方法2:内联函数

inline int Add(int x, int y)
{
    return (x + y) * 10;
}

inline 关键字使得函数在调用的地方不会生成 call 指令,而是被替换为函数的指令。假如有一个函数叫 func ,该函数经过编译后有 50 行指令,调用函数时只有一句 call 指令,如果该函数不是内联函数,在一个项目中被调用 10000 次,那么总计有 10000 + 50 行指令,如果该函数是内联函数,那么总计有 10000 * 50 行指令。指令变多会使软件的安装包变大。

inline对于编译器仅仅只是一个建议,最终是否成为inline,编译器自己决定

像类似这些函数就算加了 inline 也会被编译器否决:

1、比较长的函数 2、递归函数

inline 关键字在默认 debug 模式下不会生效,否则不方便调试。可以设置:

  

 inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。

// F.h 
#include <iostream> 
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()
{
    f(10);
    return 0;
}

// 链接错误: main.obj : error LNK2019: 无法解析的外部符号 
//"void_cdecl f(int)" (? f@@YAXH@Z),该符号在函数 _main 中被引用

解决方法是将内联函数的声明和定义都放在头文件中。 

10、nullptr

NULL 和 nullptr 都用于表示空指针,但它们在C++中有重要区别:

NULL

  • 传统C/C++中的空指针常量

  • 通常是定义为整数0的宏:#define NULL 0

  • 类型不明确,可以是整数或指针类型

  • 可能导致函数重载的不明确调用问题

nullptr

  • C++11引入的关键字

  • 明确的空指针常量,类型为std::nullptr_t

  • 不会与整数类型混淆

  • 提供更好的类型安全

void f(int)
{
    cout << "f(int)" << endl;
}
void f(int*)
{
    cout << "f(int*)" << endl;
}

int main()
{
    f(0); 
    f(NULL);

    return 0;
}

以上代码中,按我们的理解 f(0) 应该调用 f(int) ,f(NULL) 应该调用 f(int*) ,但实际上 f(0) 和 f(NULL) 都是调用的 f(int)。

在C++98中,字面常量 0 既可以是一个整形数字,也可以是无类型的指针 (void*) 常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转 (void *)0。

注意:
1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
2. 在C++11中,sizeof(nullptr)与 sizeof((void*)0)所占的字节数相同。
3. 为了提高代码的健壮性,在表示指针空值时建议最好使用nullptr。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值