AI部署通用C++知识点--面试必知必会(part1)
C++语法特性 面试必备
include 后面使用双引号""和尖括号<>的区别,以及查找路径的异同
#include "名字"
优先在当前源文件所在目录(准确说是"发出此include的那个文件的目录")查找;查找不到再去bianyiqi配置的其他目录继续查找- 常用于项目内自己的头文件
#include <名字>
直接去编译期的系统/第三方头文件目录查找,不会先看当前文件目录- 常用于标准库或已安装到系统的第三方库的头文件
- 一句话经验:项目内用
" "
,系统或第三方库用< >
不同编译器略有差异,但通用思路是: "..."
:当前文件目录 →(用户指定的包含目录)→ 系统目录<...>
: (用户指定的包含目录 见具体编译器)→ 系统目录
什么是指针,常量指针和指针常量的区别,const有什么优点?
- 在C/C++中,指针本质上存储的是一块内存的地址,它还有类型信息和解引用能力,因此可以把指针看作带类型的地址变量:
- 它的值是内存地址;
- 它知道自己指向什么类型
- 它可以通过解引用
*
直接访问那块内存
- 在C/C++中,"常量指针"和"指针常量"的名字很像,但可以通过修改权限来区分
- 常量指针
const int *p //或 int const *p;
- 含义: p是一个指针变量,可以改变它所指向的地址
- 但通过
p
不能修改它指向的值。
- 举例说明
int x = 10, y = 20; const int *p = &x; p = &y; //允许: 可以改变指向的地址 *p = 30; // 错误: 不能修改值 ```
- 如何理解记忆: 常量指针 常量不能变,
const
修饰的是*p
(即指针所指的值)
- 指针常量
int *const p = &x
;p
是一个常量指针,本身地址不能改变。- 但通过
p
可以修改它指向的值
- 举例说明
int x =10, y =20; int *const p = &x; *p = 30; // 允许:能修改指向的值 p = &y; // 错误:指针本身不可改变 ```
- 如何理解记忆:指针常量, 指针不变,即地址不变。const 紧跟指针名
p
,说明p
自身是常量
- 两者结合
const int *const p = &x;
指针和常量都不变。
- 常量指针
- 在C/C++中,const是一个非常重要的关键字,有很多优点
-
保护数据不被意外修改
- 如果变量本身应该是只读的,用
const
声明可以防止意外的写入const double PI = 3.14159 PI = 3.14; //会编译错误
- 如果变量本身应该是只读的,用
-
指针参数保护
- 在函数参数中,如果只读访问某数据,可以用
const
修饰,避免函数内部误修改。 void printStr(const char *str); //str 函数内部不会被改动,不然报错
- 在函数参数中,如果只读访问某数据,可以用
-
方便接口设计,让意图更清晰
const
是一种自带文档:看到const
就知道"这是只读的"。接口函数如果明确用const
,调用者不用担心内部会改变传入数据,更符合"接口与实现分离"的原则
-
有利于编译期优化和性能提升
- 编译期常量折叠:编译器可以把
const
变量直接替换为字面量,减少内存读取 - 寄存器优化:某些
const
变量可能直接放在寄存器里,不必反复访问内存。const int size = 10; int arr[size]; // ✅ 编译期就能确定大小
- 编译期常量折叠:编译器可以把
-
和
#define
相比的优势#define
仅仅是预处理器文本替换,没有类型检查。const
是真正的有类型变量,具有作用域和调试信息。
-
介绍下C++11的新特性
- 初始化列表(Initializer List)
-
作用:支持统一的花括号
{}
初始化语法。 -
优势:让数组、容器、对象的初始化更简洁、更安全。
int arr[3]{1,2,3}; std::vector<int>v{1,2,3,4}; struct Point {int x; int y;} Point p{10,20}
-
避免了传统
()
初始化的类型缩窄问题(narrowing)。narrowing(类型缩窄)**就是可能导致数据丢失或溢出的类型转换。**C++11 的{}
统一初始化会在编译期禁止这种不安全转换,比传统()
或=
初始化更安全。 -
举例
double d = 3.14; int x(d); // 传统 () 初始化 int y = d; // 传统 = 初始化
-
这里
d
是 double,转成 int 会丢掉小数部分。 编译器一般只是警告或直接截断成 3,而程序继续编译运行,可能埋下 bug。 -
使用
int x{3.14}; // ❌ 编译错误:从 double 到 int 缩窄
,编译器会直接拒绝编译,因为存在缩窄转换风险。
-
-
auto
关键字作用:可以这样理解,auto相当于一个占位符,让编译器根据等号有边的自动推导变量类型。
优势:减少重复书写,尤其是长而复杂的类型。auto x = 10; // 推导为 int vector<int>vec = {1,2,3,4}; auto it = v.begin(); // 推导为 std::vector<int>::iterator
另外 使用auto时必须对变量进行初始化,使用auto也可以定义多个变量,但必须注意,多个变量推导的结果必须为相同类型;
auto a; //错误,没有初始化 int a = 2; auto a =2 auto *p = &a, b = 4; // &a 为int*类型,因此auto推导的结果是int类型,b也是int类型 auto *p = &a, b = 4.5 //错误,auto推导的结果为int类型,而b推导为double类型,存在二义性
- 配合
decltype
可以写出更简洁、可维护的模板代码。 - auto使用限制
- auto 定义变量时必须初始化
- auto 不能在函数的参数中使用
- auto不能定义数组,例如:auto arr[] = “abc”, (auto arr = “abc” 这样是可以的,但arr不是数组,而是指针)
- auto 不能用于类的非静态成员变量中
- 配合
-
decltype
关键字作用:推导一个表达式的类型。
用途:常用于模板或需要保持表达式类型一致的场合。// decltype(exp表达式)变量名 [=初始值];// []表示可选 int a = 5; decltype(a) b = 10; // b 的类型与 a 相同,即 int
decltype 的使用遵循以下3条规则:
①若exp是一个不被括号()包围的表达式,或者是单独的变量,其推导的类型将和表达式本身的类型一致
②若exp是函数调用,则 decltype(exp)的类型将和函数返回值类型一致
③若exp是一个左值,或者是一个被括号()包围的值,那么 decltype(exp)的类型将是exp的引用class Base{ public: int m; }; int fun(int a, int b){ return a+b; } int main(){ int x = 2; decltype(x) y = x //y类型为int,符合① decltype(fun(x,y)) sum //sum的类型为函数fun()的返回类型,符合② Base A; decltype(A.m) a = 0; //a的类型为int decltype(A.m) b = a; //exp由括号包围,b的类型为int&,符合③ decltype(x+y)c = 0; //c 的类型为int decltype(x=x+y)d = c;//exp为左值,则d的类型为int&,符合③ return 0; }
decltype 和 auto 的区别:(两者都可以推导出变量的类型)
• auto 是根据等号右边的初始值推导出变量的类型,且变量必须初始化,auto的使用更加简洁
• decltype 是根据表达式推导出变量的类型,不要求初始化,decltype 的使用更加灵活
-
范围 for 循环(Range-based for)
作用:简化容器或数组的遍历。
std::vector<int> v{1,2,3}; // 使用冒号(:)来表示从属关系,前者是后者中的一个元素,for循环依次遍历每个元素,auto自动推导为int类型 for (auto &x : v) { x *= 2; cout << x << endl; }
- 代码更简洁,不再需要
begin()
、end()
或索引下标。
- 代码更简洁,不再需要
-
nullptr
关键字作用:提供类型安全的空指针常量。
优势:替代NULL
或0
,避免二义性。void f(int); void f(char*); f(nullptr); // 调用 f(char*),不会与 f(int) 混淆
nullptr是一种特殊类型的字面值,可以被转换成任意其他的指针类型,也可以初始化一个空指针。
-
Lambda 表达式
lambda表达式定义来一个匿名函数,一个lambda具有一个返回类型,一个参数列表和一个函数体。与函数不同的是,lambda表达式可以定义在函数类别,其格式如下:
[capture list](parameter list) -> return type{function body} // [捕获列表](参数列表)-> 返回类型{函数体}
作用:在需要的地方直接定义匿名函数。
优势:更易写出简洁的回调、算法逻辑。-
capture list(捕获列表):定义局部变量的列表(通常为空)
-
parameter list(参数列表)、return type(返回类型)、function body(函数体)和普通函数一样
-
可以忽略参数列表和返回类型,但必须包括辅获列表和酒数体
auto add = [](int x, int y) { return x + y; }; int r = add(2, 3); std::for_each(v.begin(), v.end(), [](int &x){ x *= 2; });
-
支持捕获外部变量
[=]
(按值)、[&]
(按引用)举例。#include <iostream> #include <vector> using namespace std; int main() { int a = 10, b = 20; // 使用按值捕获 [=] auto lambda = [=]() { cout << "a: " << a << ", b: " << b << endl; // a 和 b 是按值捕获的,不会被修改 }; // 调用 lambda lambda(); // 输出 a: 10, b: 20 // 修改外部变量 a = 100; b = 200; // 再次调用 lambda lambda(); // 仍然输出 a: 10, b: 20,因为按值捕获的副本不受外部修改影响 }
#include <iostream> #include <vector> using namespace std; int main() { int a = 10, b = 20; // 使用按引用捕获 [&] auto lambda = [&]() { cout << "a: " << a << ", b: " << b << endl; a *= 2; // 修改 a b += 5; // 修改 b }; // 调用 lambda lambda(); // 输出 a: 10, b: 20 cout << "After lambda call, a: " << a << ", b: " << b << endl; // 再次调用 lambda lambda(); // 输出 a: 20, b: 25 cout << "After second lambda call, a: " << a << ", b: " << b << endl; }
-
-
智能指针(Smart Pointer)
作用:自动管理动态内存,防止内存泄漏。
主要类型:std::unique_ptr
:独占所有权。- 同一时间只能有一个智能指针可以指向这个对象,但之所以说使用unique_ptr 智能指针更加安全,是因为它相比于 auto_ptr 而言禁止了拷贝操作,unique_ptr 采用了移动赋值 std::move()函数来进行控制权的转移。
std::shared_ptr
:共享所有权,引用计数。- 共享指针 share_ptr是一种可以共享所有权的智能指针,定义在头文件 memory 中,它允许多个智能指针指向同一个对象,并使用引用计数的方式来管理指向对象的指针(成员函数use_count(可以获得引用计数),该对象和其相关资源会在“最后一个引用被销毁”时候释放。
share_ptr
是为了解决auto_ptr
在对象所有权上的局限性(auto_ptr 是独占的),在使用引用计数的机制上提供了可以共享所有权的智能指针。 - 循环引用: 当两个
shared_ptr
互相引用时,它们的引用计数不会减少为 0,因为它们彼此持有对方的引用。这种情况通常发生在数据结构中,例如图或双向链表,导致内存无法释放,从而发生内存泄漏。
- 共享指针 share_ptr是一种可以共享所有权的智能指针,定义在头文件 memory 中,它允许多个智能指针指向同一个对象,并使用引用计数的方式来管理指向对象的指针(成员函数use_count(可以获得引用计数),该对象和其相关资源会在“最后一个引用被销毁”时候释放。
std::weak_ptr
:弱引用,不增加计数。weak_ptr
弱指针是一种不控制对象生命周期的智能指针,它指向一个share-ptr
管理的对象,进行该对象的内存管理的是那个强引用的share_ptr
,也就是说weak_ptr
不会修改引用计数,只是提供了一种访问其管理对象的手段,这也是它称为弱指针的原因所在.此外,weak_ptr
和share_ptr
之间可以相互转化,share_ptr
可以直接赋值给weak_ptr
,而weak_ptr
可以通过调用lock 成员函数来获得share_ptr
。weak_ptr
主要用于防止循环引用的问题。
#include <iostream> #include <memory> class A; // 前向声明 class B; // 前向声明 class A { public: std::shared_ptr<B> b; // A 持有 B 的 shared_ptr ~A() { std::cout << "A 被销毁\n"; } }; class B { public: std::weak_ptr<A> a; // 使用 weak_ptr 来避免循环引用 ~B() { std::cout << "B 被销毁\n"; } }; int main() { std::shared_ptr<A> a = std::make_shared<A>(); std::shared_ptr<B> b = std::make_shared<B>(); a->b = b; // A 持有 B b->a = a; // B 使用 weak_ptr 来观察 A,避免循环引用 // 使用 weak_ptr 的 lock() 来访问 A if (auto lockedA = b->a.lock()) { // 如果 lock 成功,lockedA 将是有效的 shared_ptr std::cout << "成功锁定 A 对象\n"; } else { std::cout << "A 对象已经被销毁\n"; } // 此时 A 和 B 都会被销毁,内存不会泄漏 }
-
左值与右值(Lvalue && Rvalue)
左值(Lvalue)
定义:左值表示内存中有一个具体的地址,它代表的是可以被修改的数据对象,通常是变量、数组元素、解引用指针等。
特点:
- 左值有明确的内存地址,可以在赋值语句的左边出现(这就是“左值”这个名字的由来)。
- 左值可以被修改(非
const
),可以对其进行赋值操作。 - 可以通过引用来访问。
举例:
int x = 10; // x 是左值 x = 20; // 可以给左值赋值 int* p = &x; // 左值的地址存储在指针 p 中 int arr[3] = {1, 2, 3}; // arr[0] 是左值 arr[0] = 10; // 可以修改
右值(Rvalue)
定义:右值是一个没有明确内存地址的临时对象,它代表的是无法直接修改的数据,通常是字面常量、临时对象、表达式的结果等。特点:
- 右值没有持久的内存地址,通常是临时的、短期存在的对象。
- 右值不能出现在赋值语句的左边(即不能作为目标变量),但可以作为右边的值进行赋值。
- 右值常常用于移动语义(如在移动构造函数中)。
举例:
int x = 10; int y = x + 5; // x + 5 是右值 int z = 20; // 20 是右值
右值的具体类型:
- 纯右值(Prvalue):表示临时对象的结果,通常是常量或表达式的计算结果。
- 例子:
5
,x + y
, 临时返回值等。
- 例子:
int x = 10; int y = 5 + x; // 5 + x 是右值(临时计算结果) std::vector<int> createVector() { std::vector<int> v = {1, 2, 3}; return v; // v 是右值,临时返回值 }
- 将亡值(Xvalue):指的是某些将要被销毁的对象(比如通过
std::move
转换的对象)。它们是一个右值,但与纯右值不同,它们通常涉及资源的转移或清理。- 例子:
std::move(a)
转换后的对象。
- 例子:
- 作用:支持移动语义和完美转发,提高性能。
std::vector<int> v1{1,2,3}; std::vector<int> v2 = std::move(v1); // 资源转移,避免深拷贝
- 在
std::move(v1)
中,v1
变成了一个将亡值,意思是v1
资源(如内存、元素等)会被转移到v2
,而不是拷贝。 std::move(v1)
是通过强制类型转换将v1
转换为右值引用(T&&
)。它并没有实际的“移动”数据,而是标记v1
为可以移动的状态。
- 左值引用与右值引用
- 左值引用(Lvalue Reference):通过
&
实现,绑定到一个左值(可以是变量或可以修改的对象)。 int a = 10; int& ref = a; // 左值引用
- 右值引用(Rvalue Reference):通过
&&
实现,绑定到一个右值,通常用于支持移动语义。 int&& rref = 5 + 2; // 右值引用绑定到右值
右值引用主要是配合移动构造函数和移动赋值运算符,可以大幅减少拷贝,提高效率。
- 左值引用(Lvalue Reference):通过
希望对你有帮助,未完待续
更多推荐
所有评论(0)