C++核心知识点全解析(一)
1. C++中值传递和引用传递的区别?
1) 值传递: 在函数调用时,会触发一次参数的拷贝动作,所以对参数的修改不会影响原始的值。如果是较大的对象,复制整个对象,效率较低。
2) 引用传递: 函数调用时,函数接收的就是参数的引用,不会触发参数的拷贝动作,效率较高,但对参数的修改会直接作用于原始的值。
2. C 和 C++ 的区别
可以考虑从以下几个方面回答:
1) 面向对象还是面向过程:
- C语言是一门面向过程的语言,侧重于通过过程(函数)来解决问题。
- C++是一门多范式语言,主要支持面向对象,侧重于使用类和对象来组织代码。
2) 继承:
- C++支持继承,允许一个子类继承一个或多个父类,达到代码复用的目的。
- C语言中没有继承的概念。
3) 函数重载:
- C++支持函数通过参数类型和参数个数的重载。
- C语言不支持重载,函数名必须唯一才行。
4) 模板:
- C++支持模板,支持静态和动态形式的多态。
- C语言对此都不支持。
5) 内存管理:
- C++使用new 和delete操作符来管理内存,也支持使用智能指针来动态管理内存。
- C语言需要使用malloc和free来申请和释放内存。
6) 标准库:
- C++的STL标准库能力比C语言丰富的多,比如vector、string、list、map等等,还有很多算法相关的能力,这些C语言都没有。
3. 什么是C++的左值和右值?有什么区别?
什么是左值? 什么是右值?
- 左值:可以出现在赋值运算符的左边,并且可以被取地址,通常是有名字的变量
- 右值:不能出现在赋值运算符的左边,不可以被取地址,表示一个具体的数据值,通常是常量、临时变量
一般可以从两个方向区分左值和右值。
方向1:
- 左值:可以放到等号左边的东西叫左值。
- 右值:不可以放到等号左边的东西就叫右值。
方向2:
- 左值:可以取地址并且有名字的东西就是左值。
- 右值:不能取地址的没有名字的东西就是右值。
示例:
int a = b + c;a是左值,有变量名,可以取地址,也可以放到等号左边,表达式 b+c 的返回值是右值,没有名字且不能取地址,&(b+c)不能通过编译,而且也不能放到等号左边。
int a = 4; // a是左值,4作为普通字面量是右值4. 什么是 C++ 的移动语义和完美转发
回答重点
移动语义和完美转发都是C++11引入的新特性。
移动语义
一种优化资源管理的机制。常规的资源管理是拷贝别人的资源。而移动语义是转移所有权,转移了资源而不是拷贝资源,性能会更好。
移动语义通常用于那些比较大的对象,搭配移动构造函数或移动赋值运算符来使用。
class A { public: A(int size) : size_(size) { data_ = new int[size]; } A(){} A(const A& a) { size_ = a.size_; data_ = new int[size_]; cout << "copy " << endl; } A(A&& a) { this->data_ = a.data_; a.data_ = nullptr; cout << "move " << endl; } ~A() { if (data_ != nullptr) { delete[] data_; } } int *data_; int size_; }; int main() { A a(10); A b = a; A c = std::move(a); // 调用移动构造函数 return 0; }如果不使用std::move,会有很大的拷贝代价,使用移动语义可以避免很多无用的拷贝,提供程序性能,C++所有的STL都实现了移动语义,方便我们使用。例如:
std::vector<string> vecs; ... std::vector<string> vecm = std::move(vecs); // 免去很多拷贝完美转发
完美转发指可以写一个接受任意实参的函数模板,并转发到其它函数,目标函数会收到与转发函数完全相同的实参,转发函数实参是左值那目标函数实参也是左值,转发函数实参是右值那目标函数实参也是右值。
那如何实现完美转发呢,答案是使用std..forward,可参考以下代码:
void PrintV(int &t) { cout << "lvalue" << endl; } void PrintV(int &&t) { cout << "rvalue" << endl; } template<typename T> void Test(T &&t) { PrintV(t); PrintV(std::forward<T>(t)); PrintV(std::move(t)); } int main() { Test(1); // lvalue rvalue rvalue int a = 1; Test(a); // lvalue lvalue rvalue Test(std::forward<int>(a)); // lvalue rvalue rvalue Test(std::forward<int&>(a)); // lvalue lvalue rvalue Test(std::forward<int&&>(a)); // lvalue rvalue rvalue return 0; }分析:
- Test (1): 1 是右值,模板中T&&t这种为万能引用,右值1传到Test函数中变成了右值引用,但是调用PrintV)时候,t变成了左值,因为它变成了一个拥有名字的变量,所以打印Ivalue而PrintV(std:.forward<T>(t))时候,会进行完美转发,按照原来的类型转发,所以打印rvalue,PrintV(std::move(t))毫无疑问会打印rvalue。
- Test(a):a是左值,模板中T&&这种为万能引用,左值a传到Test 函数中变成了左值引用,所以有代码中打印。
- Test(std::forward<T>(a)):转发为左值还是右值,依赖于T,T是左值那就转发为左值,T是右值那就转发为右值。
5. 什么是 C++ 的列表初初始化
回答重点
C++11中引入了列表初始化,它的语法比较简单,就是可以使用花括号0来初始化变量或对象。列表初始化可以应用于内置类型、用户自定义类型(类、结构体等)以及其它容器等。它有以下几点好处:
- 方便,基本上可以替代普通括号初始化
- 可以使用初始化列表接受任意长度
- 可以防止类型窄化,避免精度丢失的隐式类型转换
下面深入看下列表初始化的几个用法:
1. 基础数据类型、
int a{10}; // 列表初始化 int a = {19}; // 列表初始化(也可以不使用等号)2. 初始化数组
int arr[3] = {1, 2, 3}; // 使用花括号初始化数组3. 类对象初始化,构造函数需要支持列表初始化
class Point { public: int x, y; Point(int a, int b) : x{a}, y{b} {} }; Point p{1, 2}; // 使用花括号初始化对象4. 容器初始化
std::vector<int> vec = {1, 2, 3, 4};5. 防止类型窄化
int x{3.14}; // error,float转int会触发类型窄化6. 聚合类型的列表初始化
聚合类型是指没有用户定义的构造函数、没有私有或受保护的非静态数据成员、没有基类以及没有虚函数的类、结构体或联合体。对于聚合类型,列表初始化会直接按顺序初始化其成员。
struct Aggregate { int a; double b; }; Aggregate agg{1, 2.3}; // 初始化a为1,b为2.3扩展知识
什么是类型窄化?
- 从浮点类型到整数类型的转换
- 从long double到double或float的转换,以及从double 到float的转换,除非源是常量表达式且不发生溢出
- 从整数或无作用域枚举类型到不能表示原类型所有值的整数类型的转换,除非源是其值能完全存储于目标类型的常量表达式
示例代码:
int main() { int a = 1.2; // ok int b = {1.2}; // error float c = 1e70; // ok float d = {1e70}; // error float e = (unsigned long long)-1; // ok float f = {(unsigned long long)-1}; // error float g = (unsigned long long)1; // ok float h = {(unsigned long long)1}; // ok const int i = 1000; const int j = 2; char k = i; // ok char l = {i}; // error char m = j; // ok char m = {j}; // ok,因为是const类型,这里如果去掉const属性,也会报错 }std::initializer list
我们平时开发使用STL过程中可能发现它的初始化列表可以是任意长度,有没有想过它是怎么实现的
呢?
答案是 std:initializer_list,看这段代码:
struct CustomVec { std::vector<int> data; CustomVec(std::initializer_list<int> list) { for (auto iter = list.begin(); iter != list.end(); ++iter) { data.push_back(*iter); } } };std:initializerlist<T>,它可以接收任意长度的初始化列表,但是里面必须是相同类型T,或者都可
以转换为T。
6. C++ 中 move 有什么作用?它的原理是什么?
回答重点
move是C++11引入的一个新特性,用来实现移动语义。它的主要作用是将对象的资源从一个对象转移到另一个对象,而无需进行深拷贝,减少了资源内存的分配,可提高性能。它的原理很简单,我们直接看它的源码实现:
// move template <class T> LIBC_INLINE constexpr cpp::remove_reference_t<T> &&move(T &&t) { return static_cast<typename cpp::remove_reference_t<T> &&>(t); }从源码中你可以看到,std::move的作用只有一个,无论输入参数是左值还是右值,都强制转成右值。
7. 介绍 C++ 中三种智能指针的使用场景?
回答重点
C++中的智能指针主要用于管理动态分配的内存,避免内存泄漏。
C++11标准引入了三种主要的智能指针:std::unique_ptr std::shared_ptr std::weak_ptr
1. std::unique_ptr
std::unique_ptr是一种独占所有权的智能指针,意味着同一时间内只能有一个unique_ptr指向一个特定的对象。当unique_ptr被销毁时,它所指向的对象也会被销毁。
使用场景:
- 当你需要确保一个对象只被一个指针所拥有时。
- 当你需要自动管理资源,如文件句柄或互斥锁时。
示例代码:
#include <iostream> #include <memory> class Test { public: Test() { std::cout << "Test::Test()\n"; } ~Test() { std::cout << "Test::~Test()\n"; } void test() { std::cout << "Test::test()\n"; } }; int main() { std::unique_ptr<Test> ptr(new Test()); ptr->test(); // 当ptr离开作用域时,它指向的对象会被自动销毁 return 0; }2. std::shared_ptr
std::shared_ptr是一种共享所有权的智能指针,多个shared_ptr可以指向同一个对象。内部使用引用计数来确保只有当最后一个指向对象的shared_ptr被销毁时,对象才会被销毁。
使用场景:
- 当你需要在多个所有者之间共享对象时。
- 当你需要通过复制构造函数或赋值操作符来复制智能指针时。
示例代码:
#include <iostream> #include <memory> class Test { public: Test() { std::cout << "Test::Test()\n"; } ~Test() { std::cout << "Test::~Test()\n"; } void test() { std::cout << "Test::test()\n"; } }; int main() { std::shared_ptr<Test> ptr1(new Test()); std::shared_ptr<Test> ptr2 = ptr1; ptr1->test(); // 当ptr1和ptr2离开作用域时,它们指向的对象会被自动销毁 return 0; }3. std::weak_ptr
std::weak_ptr是一种不拥有对象所有权的智能指针,它指向一个由std::shared_ptr管理的对象。weak_ptr用于解决 shared_ptr之间的循环引用问题。
使用场景:
- 当你需要访问但不拥有由shared_ptr管理的对象时。
- 当你需要解决shared_ptr之间的循环引用问题时。
- 注意weak_ptr肯定要和 shared_ptr搭配使用。
示例代码:
#include <iostream> #include <memory> class Test { public: Test() { std::cout << "Test::Test()\n"; } ~Test() { std::cout << "Test::~Test()\n"; } void test() { std::cout << "Test::test()\n"; } }; int main() { std::shared_ptr<Test> sharedPtr(new Test()); std::weak_ptr<Test> weakPtr = sharedPtr; if (auto lockedSharedPtr = weakPtr.lock()) { lockedSharedPtr->test(); } // 当sharedPtr离开作用域时,它指向的对象会被自动销毁 return 0; }