C++八股文笔记:【入门必备概念】和【代码示例】
C++八股文笔记:【入门必备概念】和【代码示例】
-
- 0 声明
- 1 什么是虚函数?
- 2 C++中,声明虚函数时候,编译器会自动生成一个虚函数表(vtable)以及一个虚函数指针(vptr),请解释Vtable和vptr是如何工作的
- 3 面向对象编程(OOP)的三大特性
- 4 构造函数和析构函数的区别
- 5 引用和指针的区别
- 6 C++中,关键字class和struct的区别
- 7 前置递增和后置递增的区别
- 8 C++中常见的强制转型方式有那些?及其区别
- 9 谈一谈const在C++中的作用
- 10 `const int* ptr` 和 `int* const ptr` 有什么区别?
- 11 浅拷贝和深拷贝的区别
- 12 为什么拷贝构造函数必须是const,引用,而不是直接传值?
- 13 局部静态变量在C++中的生命周期
- 14 什么是智能指针,C++标准库中提供了那几个?以及它们之间的区别
- 15 什么是RAII(资源获取即初始化)
- 16组合和继承的关系
- 17 列举并解释C++中常见模式
- 18 解释RTTI在c++中的作用
- 19 什么是lambda表达式,它在c++中的作用是什么?
- 20 键字mutable的作用是什么?
- 21 解释一下互斥锁和自旋锁的区别
- 22 什么是线程池
- 23 解释一下 std::future 和 std::promise 是如何协同工作的?
- 24 请阐述 std::atomic_flag的主要特点和用途
- 25 为什么基类的析构函数建议使用虚函数(即声明为vitrtual)
- 26 简述std::vector的扩充策略
- 27 std::map和std::unordered_map的区别是什么?
- 28 std::priority_queue是按照什么顺序排序的?如何获取优先级最高的元素?
- 29 什么是左值引用和右值引用?
- 30 什么是移动语义(move semantics) 和 完美转发(perfect forwarding),它们在c++中输入和实现的
- 31 在c++编程中,模板元编程(Template Metaprogramming,TMP)是什么?给出一个例子
- 32 extern ”C“ 声明在C++中的作用是什么?
0 声明
作为C++ 的初学者,在了解完c++的基础语法之后,不仅要学会如何使用,还需要知道为什么使用、在哪里使用!就近说这对找工作很重要,远的说对以后深入研究也很重要;本文总结C++的一些基础和必备的知识,并加入了代码方便自己和网友了解。内容参考了B站【阿西拜编程】的C++岗位面试视频内容,向其表达感谢,谢谢。后续会根据学习对内容进行补充,完善。如果有错误欢迎您批评指正!
1 什么是虚函数?
在 C++ 中,虚函数 是一种允许运行时多态的成员函数。当一个类的成员函数被声明为 virtual 时,通过基类指针或引用调用这个函数时,会根据对象的实际类型(而不是指针类型)来决定调用哪个版本。
2 C++中,声明虚函数时候,编译器会自动生成一个虚函数表(vtable)以及一个虚函数指针(vptr),请解释Vtable和vptr是如何工作的
虚函数表:vtable是实现C++多态的一种机制,每个包含虚函数的类都会有一个虚函数表,它是一个函数指针数组。表中存放着虚函数的地址。当通过基类指针或者引用调用虚函数的时候,程序会调用对象的虚函数表来查找正确的寻函数地址并调用。
虚函数指针:vptr是一个存储在对象中的指针,指向该类的Vtable。每个包含虚函数的类的对象都会有一个vptr。当对象创建的时候,对象的vptr会被初始化为指向该类的虚函数表。当通过基类指针或引用调用虚函数的时候,会通过vptr访问虚函数表,从而实现多态。

代码示例:
#include <iostream>
// 基类
class Base {
public:
// 虚函数:将被放入虚函数表中
virtual void show() {
std::cout << "Base::show() called" << std::endl;
}
// 构造函数(为了展示,我们不主动初始化vptr,编译器会自动处理)
Base() {
// 实际上,编译器在这里会隐式为 this 对象设置 vptr,
// 指向 Base 类的虚函数表(vtable)。
}
// 虚析构函数也是常见做法,确保通过基类指针删除派生类对象时行为正确
virtual ~Base() {}
};
// 派生类
class Derived : public Base {
public:
// 重写基类的虚函数
void show() override {
std::cout << "Derived::show() called" << std::endl;
}
// 构造函数中,编译器会把 vptr 改为指向 Derived 的虚函数表
Derived() {
// vptr 被设为指向 Derived 类的虚函数表
}
};
// 辅助函数:用来模拟“我们如何通过对象找到 vptr 和 vtable”(仅作概念演示,非真实代码)
void explainVTableConcept() {
std::cout << "\n===== 概念解释:vptr 和 vtable 如何工作 =====\n";
Base* obj = new Derived();
obj->show(); // 输出:Derived::show() called
delete obj; // 调用虚析构函数,也通过 vtable 找到正确的析构函数
}
int main() {
explainVTableConcept();
return 0;
}
3 面向对象编程(OOP)的三大特性
封装:将数据和操作这些数据的函数组合在一起,并隐藏内部实现的细节
继承:允许新的类继承现有的类,从而实现代码的复用和层次结构
多态:多态主要通过虚函数实现,即一个接口可以有多中不同的实现;可以用统一的方法来处理不同的类型的对象
运行时多态和编译时多态:
运行时多态是在程序运行时动态确定函数调用的,它依赖于与函数
编译时多态实在编译期间静态确定函数调用的,它依赖于重载和模板
4 构造函数和析构函数的区别
构造函数初始化对象,析构函数用于销毁对象
特别:初始化和赋值的区别:初始化是从无到有,赋值是本来有对象现在给它新的值
5 引用和指针的区别
指针是有实体的,是一个对象;而引用是对象的别名,一旦引用被初始化了某个对象,就不能改变为指向另一个对象,且引用必须初始化。指针本身就是对象,它存储了另一个对象的地址。本质上指针就是一个值为地址的普通对象而已,所以他可以初始化后在进行修改,甚至可以初始化空指针不指向任何对象。
特别加分点:存在指针的指针,不存在引用的引用
指针支持指针运算,如增加指针的值(pointer+1)来移动到下一个元素,但引用不可以。
6 C++中,关键字class和struct的区别
成员默认的访问权限不同,struct为public,class为private
使用习惯上:class更常用,特别是在OOP特性时,struct通常用于简单的数据结构。
7 前置递增和后置递增的区别
前++i 是先 加加再返回,返回的是加加后的结果,i的值和返回的值是同一个值
后i++ 是先 返回在加加,返回的是加加前的结果,i的值和返回的值不是同一个值
8 C++中常见的强制转型方式有那些?及其区别
static_cast:静态强制类型转换,在编译时进行类检查
dynamic_cast:动态强制类型转换,主要用于安全的进行基类和派生类的之间的向上和向下转型
reinterpret_cast:用于低级别的强制类型,不进行任何类型的检查
const_cast:用于移除后添加const
C风格转型(旧式转型):类似于static_cast,但是没有明确指定转型的类型。
static_cast和dynamic_cast的区别:表面上来说,一个是静态检查,一个是动态检查;动态试运行起来以后,通过内存里面对象的类型来进行判断,dynamic_cast会用到RT TI(run time type identification)的技术,因为要进行类识别,dynamic_cast的运行效率要低于static_cast,但是更安全。
什么情况下一定要使用dynamic_cast:在多态的上下文下,如果不确定转换的对象的具体类型(动态类型),就应该进行dynamic进行检查,否则有可能不小心将基类类型的对象,通过static_cast转换为错误的派生类的类型,造成内存崩溃。
代码解释:
class Base {
public:
virtual ~Base() {}
};
class Derived : public Base {
public:
void doSomething() {}
};
int main() {
Base* basePtr = new Base();
Derived* derivedPtr = static_cast<Derived*>(basePtr); // 错误的转换, 假设基类对象为派生类对象
derivedPtr->doSomething(); // 未定义行为、运行时错误
delete basePtr;
// 示范正确的用法
Base* anotherBasePtr = new Derived(); // 假设这是一个实际的派生类对象
Derived* anotherDerivedPtr = dynamic_cast<Derived*>(anotherBasePtr);
if (anotherDerivedPtr) { // 转换成功则指针非空
anotherDerivedPtr->doSomething(); // 转换成功才执行
}
delete anotherBasePtr;
return 0;
}
9 谈一谈const在C++中的作用
修饰符:修饰变量和对象,表明它们的值在初始化后不可以被修改,例如 const int value =10;
限制符:限制成员函数,不能修改类的类的非静态成员变量,也不能调用其他的非const成员函数。一个声明为const的成员函数保证不会修改对象的状态,因此它可以在const对象上被调用
class MyClass {
public:
void myMethod() const;/*这个函数保证不会修改对象的状态*/}
};
顶层const和底层const的区别:
底层const:当const位于星号()左边,它被称为底层const。它指示指针所指向的对象是不可变的,即
不能通过这个指针来修改对象的内容:const int ptr;
顶层const:当const位于星号()右边,它被称为顶层const。它指示指针本身是不可变的,即不能指向
另一个不同的地址,但可以通过这个指针修改它所指向的对象的内容:int const ptr;
10 const int* ptr 和 int* const ptr 有什么区别?
口诀:const 修饰谁,谁就不能改。
| 声明方式 | 中文解释 | 能否改指向 | 能否改内容 |
|---|---|---|---|
const int* ptr |
指向常量的指针 | ✅ 可以 | ❌ 不可以 |
int* const ptr |
常量指针 | ❌ 不可以 | ✅ 可以 |
const int* const ptr |
指向常量的常量指针 | ❌ 不可以 | ❌ 不可以 |
记忆小技巧
const在*左边 → 所指内容不能改(内容是常量)const在*右边 → 指针本身不能改(指针是常量)
11 浅拷贝和深拷贝的区别
浅拷贝:指 按位复制对象的成员,包括指针的值(即地址),而 不复制指针所指向的实际内容。
简单来说:浅拷贝只是简单地复制了指针本身,两个对象的指针成员将指向同一块内存。
深拷贝:不仅复制对象的成员,而且 为指针成员分配新的内存,并复制指针指向的内容,从而让 新对象拥有独立的数据副本。
简单来说:深拷贝会 新开辟一块内存,并把原对象指针指向的内容复制过去,这样两个对象的指针成员指向的是 不同的内存区域,但内容相同。
#include <iostream>
#include <cstring>
using namespace std;
class Person {
private:
char* name;
public:
Person(const char* n) {
// 分配动态内存
name = new char[strlen(n) + 1];
strcpy(name, n);
}
// ✅ 深拷贝构造函数
Person(const Person& other) {
cout << "调用深拷贝构造函数" << endl;
name = new char[strlen(other.name) + 1]; // 重新分配内存
strcpy(name, other.name);
}
void show() const {
cout << "name: " << name << endl;
}
~Person() {
delete[] name;
}
};
int main() {
Person p1("Alice");
Person p2 = p1; // 调用拷贝构造函数(深拷贝)
p1.show();
p2.show();
return 0;
}
补充:
| 函数声明 | 类型 | 作用 |
|---|---|---|
Person(string name, int age) |
普通构造函数 | 直接创建对象 |
Person(const char* n) |
普通构造函数(参数不同) | 允许传 C 字符串 |
Person(const Person& other) |
拷贝构造函数 | 用已有对象创建新对象 |
12 为什么拷贝构造函数必须是const,引用,而不是直接传值?
引用:避免无限递归,如果拷贝构造函数使用传值的方式, 那么每次调用拷贝构造函数时,都会创建一个新的临时对象,这个新对象又需要调用拷贝构造函数来创建它的副本,从而导致无限递归。
const限制:拷贝构造函数的目的是复制对象,而不是修改它。通过使用const引用,可以确保在构造过程中传入的对象保持不变;使用const引用可以同时接受左值和右值
特:c++中对象可以使左值和右值。左值代表对象的身份,而右值代表对象的值,如果拷贝构造函数的参数不是const,那么他只能绑定在左值上,因为非const引用不能绑定在右值上。
13 局部静态变量在C++中的生命周期
局部静态变量的作用域仅限于声明它的函数内部,它不能被其他函数或代码块访问
当函数第一次被调用时,该变量会被初始化,后续调用是不能再重新初始化,直到程序结束才会被销毁
特:多线程环境下,局部静态变量的初始化过程是安全的,可以确保只有一个线程完成初始化操作
14 什么是智能指针,C++标准库中提供了那几个?以及它们之间的区别
智能指针是C++标准库提供的模板类,用于自动管理动态内存的分配,包括std::unique_ptr、std::shared_ptr、std::weak_ptr;
unique_ptr独占所有权、shared_ptr共享所有权、weak_ptr拥有所有权
unique_ptr不可拷贝,只能移动;shared_ptr可以拷贝和移动;weak_ptr可以拷贝和移动,它本身不影响对象的生命周期,它是一种访问shared_ptr管理的对象的弱引用方式
weak_ptr存在意义是什么?
shared_prt存在循环引用的问题,即两个或多个shared_ptr智能指针对象相互引用对方,会造成谁的计数器都不会归零的情况,造成内存泄露的情况。
weak_ptr不参与计数,会不会产生空悬指针的问题?
weak_ptr提供了两种方式:expired()和lock()
其中expired()允许检查其指向的对象是否仍然存在;lock()会临时将weak_ptr转化为shared_ptr,在转换前lock()也会检查weak_ptr指向的对象是否存在
**特别:**当创建一个shared_ptr智能指针对象的时候我鼓励使用std::make_shared,它是一个专门来创建std::shared_ptr的工厂,但使用std::make_shared只需要一次内存分配(对象本身和控制块)(先在堆上创建一个动态内存,创建一个对象,用一个普通的指针指向它,再把它交给智能指针),所以性能更好。并且使用了RAII,直接创建对象并将其管理权交给std::shared_ptr
//推荐写法
std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(args);
//不推荐
MyClass* rawptr =new MyClass(args);
std::shared_ptr<MyClass> ptr2 = std::shared_ptr<MyClass>(rawptr);
rawptr = nullptr;
15 什么是RAII(资源获取即初始化)
RAII(Resource Acquisition Is Initialization )是C++中一种资源(如内存、文件)管理策略,它将资源的生命周期与对象的生命周期绑定在一起,这意味着资源的获取和释放都通过对象的构造函数和析构函数来管理,从而保证资源在对象声明周期结束时候自动释放。
16组合和继承的关系
继承:是一种“is-a”关系,继承另一个类(基类或父类)的属性和操作
组合:一种“has-a”,包含另一个类的对象作为成员
class Engine {
// 发动机相关的属性和操作
};
class Tire {
// 轮胎相关的属性和操作
};
class Car : public Engine, public Tire {
};
// 应该使用组合
class Car {
private:
Engine engine;
Tire tires[4];
};
//错误
//class Car:public Engine,public Tire{ };
17 列举并解释C++中常见模式
单例模式:单例模式确保一个类只能创建一个对象,提供全局访问(会造成代码结构混乱,耦合性高难以维护)
工厂模式:工厂模式会通过定义一个公共的接口来创建对象,并由子类决定实例化那个具体的类
观察者模式:观察者模式定义了一种对象间一对多的关系,当一个对象的状态发生变化时,其实所有的依赖者都会收到通知并自动更新。这种模式被广泛的应用于事件处理和发布-订阅系统中
策略模式:策略模式定义了一族算法,并将每个算法封封装成独立的类,使得它们可以相互替换
模板方法模式:模板方法模式定义了一个抽象类,并将某些步骤延伸到子类中实现
设计模式原则:
单一职责:每个类只负责一个功能
开放封闭:类应该对扩展开放,对修改关闭。容易扩展。但不通过修改现有的代码来增加新的功能
依赖倒置:高曾代码不应该依赖底层代码;是依赖于接口,而不是具体的实现
优先使用组合而不是继承:很多情况下,组合比继承更具有灵活性
里式替换原则:子类对象必须能够替换它们的父亲对象,而不影响程序的行为。
class Shape { //遵循开放封闭原则, 定义了一个可以被继承和扩展的接口, 而不需要修改 Shape 类本身的代码
public:
virtual int area() const { return 0; }
};
class Circle : public Shape { // 子类1: Circle, 遵循里氏替换原则
public:
Circle(int radius) : radius_(radius) {}
int area() const override { // 重写计算面积的函数
return 3 * 14 * radius_ * radius_; // 注意: 通常圆周率用 M_PI 或者更精确的值
}
private:
int radius_;
};
// printArea 也遵循开放封闭原则, 并不依赖于Shape类的特定子类。它对扩展开放,
// 因为它可以接受任何 Shape 类型的实例, 只要这个实例实现了 area() 函数
void printArea(const Shape& shape) { // 遵循依赖倒置原则, 它依赖于Shape类型
std::cout << "The area is: " << shape.area() << std::endl;
}
int main() {
// 遵循了单一职责原则。Shape 类负责定义形状的接口, Circle 负责实现特定形状的行为。
// printArea 函数负责打印给定形状的面积
Circle circle(5);
printArea(circle);
return 0;
}
18 解释RTTI在c++中的作用
c++中RTTI(运行时类型识别)允许在运行时获取对象的类型信息
因为C++是静态类型语言,通常在编译时类型就确定。但是在多态的情况下,可能需要运行是确定对象的实际类型
19 什么是lambda表达式,它在c++中的作用是什么?
lambda表达式是没有名字(匿名)的调用对象,在需要一个可调用对象的地方进行快速定义
std::vector<int> vec = { 5, 3, 9, 1, 4 };
// 使用 lambda 表达式进行降序排序
std::sort(vec.begin(), vec.end(), [](int a, int b) { return a > b; });
// 使用 lambda 表达式打印向量中的元素
std::for_each(vec.begin(), vec.end(), [](int n) { std::cout << n << " "; });
Lambda表达式的基本语法如下:
[capture list](parameters) -> return_type {
// 函数体
}
//- capture list: 捕获列表, 用于指定lambda表达式中所使用的外部变量
//- parameters: 参数列表, 与普通函数一样, 在括号内指定参数名称及其类型
//- return_type: 返回类型, 如果省略, 则根据返回值推导
**问题:**请解释一下在下面的代码中, lambda 表达式对于外部变量 x 的捕获方式是什么, 以及输出结果是什么?
int x = 10;
auto lambda = [x]() { std::cout << "Inside lambda: x = " << x << std::endl; };
x = 20;
lambda();
值捕获,当创建 lambda 表达式时, 它捕获了当时 x 的值 10。之后即使修改了外部的 x 为 20, 在 lambda 表达式内部使用的仍然是捕获时的值 10。因此输出结果为:“Inside lambda: x = 10”。
auto lambda = [&x]() { std::cout << "Inside lambda: x = " << x << std::endl; };
//引用方式,结果为20
auto lambda = [&]() { std::cout << "Inside lambda: x = " << x << std::endl; };
//引用所有变量,结果为20
auto lambda = [=]() { std::cout << "Inside lambda: x = " << x << std::endl; };
//拷贝所有值,结果为10
20 键字mutable的作用是什么?
tips:在计算机中常量性分为按位常量性和逻辑常量性,编译器默认执行按位常量性,只要修改const成员函数不影响整体的逻辑,就可以使用mutable
mutable关键字可以应用于类中定义的任何成员变量,并且表示这个成员变量可以在const成员函数中被修改。
class MyClass {
public:
MyClass() : counter(0) {}
void constMemberFunction() const { counter++; }
void regularMemberFunction() { counter++; }
private:
mutable int counter; // 可以被const成员函数修改
};
int main() {
MyClass myObject;
myObject.constMemberFunction(); // 合法,counter被修改
myObject.regularMemberFunction(); // 合法,counter也被修改
}
21 解释一下互斥锁和自旋锁的区别
互斥锁和自旋锁是两种不同的同步机制,在多线程编程过程中用于控制对共享资源的访问。
互斥锁在获取锁失败时会堵塞线程,会让出CPU的控制权,而自旋锁会不断尝试获取锁。互斥锁通常是更好地选择
自旋锁适合于持锁时间短的线程且不想在等待时间让出CPU的使用权的场景,这样会避免切换线程的开销,但会增加CPU的使用。
22 什么是线程池
线程池是一种多线程处理形式,他预选创建一定数量的线程放入线程池,当有任务组要执行时,从线程池中取出线程来执行任务,任务完成后线程不会销毁,而是返回到线程池中等待下一任务。
避免了频繁创建和销毁下带来的开销,提高了性能
能够限制并发线程的数量,防止过多线程同时运行导致的系统资源耗尽和性能下降
举例:创建一个ThreadPool类,包含一个固定大小的线程池容器,并提供了一个任务队列。线程池中的每个线程都在一个循环中等待任务队列中的任务,并执行它们。当线程池被销毁时,它会停止所有线程并等待它们完成。
23 解释一下 std::future 和 std::promise 是如何协同工作的?
它们是C++ 并发编程( 头文件 里非常重要的一对“通信工具”。std::promise 和 std::future 是一对“一发一收”的机制。
std::promise用于在线程间传递数据或状态,它可以设置一个值或异常。std::future 则用于获取由 std::promise 设置的值或异常。当一个线程通过 std::promise 设置了值或异常后,另一个通过关联的 std::future 可以获取到这个结果。std::promise 是一个只能移动(move-only)的类型
#include <iostream>
#include <thread>
#include <future>
using namespace std;
// 子线程函数:计算平方
void computeSquare(std::promise<int> resultPromise, int value) {
cout << "[子线程] 正在计算平方..." << endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
resultPromise.set_value(value * value); // 把结果放进 promise
}
int main() {
std::promise<int> prom; // 创建 promise(负责“写入结果”)
std::future<int> fut = prom.get_future(); // 从 promise 生成 future(负责“读取结果”)
std::thread t(computeSquare, std::move(prom), 10); // 将 promise 交给子线程
cout << "[主线程] 等待结果..." << endl;
int result = fut.get(); // 等待结果(阻塞直到子线程调用 set_value)
cout << "[主线程] 收到结果:" << result << endl;
t.join();
return 0;
}
24 请阐述 std::atomic_flag的主要特点和用途
std::atomic_flag 是 最底层、最轻量、最原始 的原子同步工具。
它的主要用途是实现 自旋锁 或 线程安全标志位。
操作只有两个:test_and_set()(占用) 和 clear()(释放)。
经典的自旋锁案例:
#include <iostream>
#include <thread>
#include <atomic>
using namespace std;
std::atomic_flag lockFlag = ATOMIC_FLAG_INIT; // 全局锁标志
void spin_lock() {
while (lockFlag.test_and_set(std::memory_order_acquire)) {
// 如果标志已被占用(返回true),则一直等待(自旋)
}
}
void spin_unlock() {
lockFlag.clear(std::memory_order_release); // 释放锁
}
int counter = 0;
void work(int id) {
for (int i = 0; i < 1000; ++i) {
spin_lock(); // 获取锁
++counter; // 临界区
spin_unlock(); // 释放锁
}
}
int main() {
std::thread t1(work, 1);
std::thread t2(work, 2);
t1.join();
t2.join();
cout << "最终结果:" << counter << endl;
}
25 为什么基类的析构函数建议使用虚函数(即声明为vitrtual)
避免资源泄露:派生类中可能拥有一些基类中未定义的资源(如动态分配的内存)。如果基类的析构函数不是虚函数,那么通过基类指针删除派生类对象时,只会调用基类的析构函数,而不会调用派生类的析构函数。
26 简述std::vector的扩充策略
vector的扩容策略通常适当现有的容量不足时,会分配比当前更大的新空间,将原有的元素复制到新的空间中,把旧的内存空间释放。新的容量肯是原容量的一定倍数(2倍)。
27 std::map和std::unordered_map的区别是什么?
std::map是基于红黑树的,确保了元素的有序性
std::unordered是基于哈希实现的,不保证元素的有序性
28 std::priority_queue是按照什么顺序排序的?如何获取优先级最高的元素?
std::priority_queue 是 C++ STL 中的 优先队列容器适配器,它的排序逻辑和“堆(heap)”密切相关。
核心特征:元素始终按照“优先级顺序”排列,队首(top())永远是优先级最高的元素。(默认最大元素在顶)
#include <iostream>
#include <queue>
using namespace std;
int main() {
std::priority_queue<int> pq;
//等价于std::priority_queue<int, std::vector<int>, std::less<int>> pq;
//(最小值优先:std::priority_queue<int, std::vector<int>, std::greater<int>> pq;)
pq.push(5);
pq.push(2);
pq.push(8);
pq.push(3);
cout << "优先级最高(最大)的元素是: " << pq.top() << endl;
pq.pop(); // 删除最高优先级的元素
cout << "下一个最高优先级元素是: " << pq.top() << endl;
return 0;
}
//优先级最高(最大)的元素是: 8
//下一个最高优先级元素是: 5
29 什么是左值引用和右值引用?
左值引用(T&) 是绑定可持久存在的对象(变量);右值引用(T&&) 是绑定临时对象,用于高效转移资源。
特:std::move() 把左值强制当作右值使用(常用于移动语义)。
它们是掌握 移动语义(move semantics) 和 完美转发(perfect forwarding) 的关键。
| 类型 | 定义 | 能绑定什么 | 示例 |
|---|---|---|---|
| 左值引用(T&) | 引用一个“有名字”的对象 | 左值 | int& a = x; |
| 右值引用(T&&) | 引用一个“临时值” | 右值 | int&& b = 5; |
代码示例
#include <iostream>
using namespace std;
void printLValue(int& x) { // 左值引用版本
cout << "左值引用函数被调用,x = " << x << endl;
}
void printRValue(int&& x) { // 右值引用版本
cout << "右值引用函数被调用,x = " << x << endl;
}
int main() {
int a = 10;
printLValue(a); // ✅ a 是左值
// printRValue(a); // ❌ 错误:a 是左值,不能绑定到右值引用
printRValue(20); // ✅ 20 是右值
// printLValue(20); // ❌ 错误:20 是右值,不能绑定到左值引用
// 使用 std::move 将左值强制转换为右值
printRValue(std::move(a)); // ✅ OK,a 被“视作右值”
}
30 什么是移动语义(move semantics) 和 完美转发(perfect forwarding),它们在c++中输入和实现的
移动语义:移动语义是 C++11 引入的机制,允许把资源(如内存、文件句柄)从一个对象转移给另一个对象,而不是“复制”一份。
移动构造函数示例:
#include <iostream>
#include <utility> // for std::move
using namespace std;
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t n) : size(n), data(new int[n]) {
cout << "构造 " << size << " 个元素\n";
}
~Buffer() { delete[] data; }
// 拷贝构造(深拷贝)
Buffer(const Buffer& other) : size(other.size), data(new int[other.size]) {
cout << "拷贝构造\n";
for (size_t i = 0; i < size; ++i)
data[i] = other.data[i];
}
// ✅ 移动构造(资源转移)
Buffer(Buffer&& other) noexcept : size(other.size), data(other.data) {
cout << "移动构造\n";
other.data = nullptr; // 转移所有权
other.size = 0;
}
};
int main() {
Buffer a(5);
Buffer b = std::move(a); // 触发移动构造,而不是拷贝构造
}
完美转发:是一种模板技术,能在函数模板中“保持参数原本的左/右值属性”并传递给另一个函数。完美转发通过引用和std::forward实现,std::forword是一个实现转发的函数模板。完美转发利用了引用折叠的规则来确保参数的原始值得以保留。用“万能引用(T&&)” + std::forward<T>():
代码:
template <typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg)); // ✅ 保留原始值类别
}
void process(int& x) {
cout << "处理左值: " << x << endl;
}
void process(int&& x) {
cout << "处理右值: " << x << endl;
}
int main(){
int x = 10;
wrapper(x); // 传左值 → 调用 process(int&)
wrapper(20); // 传右值 → 调用 process(int&&)
}
31 在c++编程中,模板元编程(Template Metaprogramming,TMP)是什么?给出一个例子
在c++编程中,模板元编程是一种编译时进行计算和生产代码的技术
主要特点:模板提供了静态多态性,可以在编译时解决函数重载和模板实例化的问题,同时,计算是在编译是完成的,提高了运行的效率,能够实现高度灵活和通用的代码。
模板元编程阶乘:
#include<iostream>
using namespace std;
// 主模板:定义 func<N>,其中 N 是一个编译期常量整数
template<int N>
struct func {
static const int value = N * func<N - 1>::value;
};
// 特化模板:当 N == 0 时,递归终止
template<>
struct func<0> {
static const int value = 1;
};
int main() {
cout << "5!=" << func<5>::value << endl;
}
c++11中替代方案(推荐):用 constexpr函数
#include<iostream>
using namespace std;
// C++11 起:constexpr 函数,编译期计算阶乘
constexpr int factorial(int n) {
if (n == 0)
return 1;
return n * factorial(n - 1);
}
int main() {
cout << "5!=" << factorial(5) << endl; // 输出 120
}
32 extern ”C“ 声明在C++中的作用是什么?
告诉C++编译器请按照 C 语言的规则来编译和链接这段代码(通常是函数或变量),不要对它进行 C++ 的名称修饰。目的是为了让 C++ 代码能够与 C 语言代码互相调用(混合编程)。
原因:C++ 支持函数重载,C 不支持, 在C++ 中,我们可以定义多个 同名函数,但参数不同
// C++ 允许函数重载
void foo(int a);
void foo(double a);
//在编译后可能_Z3fooi→ 表示 foo(int),_Z3food→ 表示 foo(double)
而C 语言没有函数重载,所以 C 编译器不会对函数名进行名称修饰
更多推荐

所有评论(0)