温馨提示:这篇文章已超过408天没有更新,请注意相关的内容是否还可用!
摘要:本篇文章介绍了八股文在C++编程中的应用,内容全面详尽。涵盖了C++编程中八股文的定义、特点、使用场景等方面,提供了丰富的知识和实例,帮助读者更好地理解和掌握八股文在C++编程中的实际应用。文章内容丰富,适合对C++编程和八股文感兴趣的读者阅读。
文章目录
- const
- 说说const int *a, int const *a, const int a, int *const a, const int *const a分别是什么,有什么特点。
- const成员函数
- const和#define的区别
- 什么const在常量区,什么const在栈区,什么const放入符号表优化
- 虚函数
- 作用
- 实现
- 纯虚函数
- 虚函数在什么时候调用?
- 大小
- C++ 中哪些函数不能被声明为虚函数?
- 为什么虚函数不能是模板函数?
- 虚函数表既然希望类的所有对象共享为什么不放在全局区
- 菱形继承
- 类型转换
- static_cast
- dynamic_cast
- dynamic_cast和虚函数的区别
- reinterpret_cast
- const_cast
- volatile关键字
- 构造函数一大家子
- 拷贝构造函数
- 什么时候调用拷贝构造函数?
- 析构函数
- 为什么要用虚的?
- 移动构造函数
- C++类内是否可以定义引用?
- 模板类
- 模板实例化
- 模板具体化
- 模板为了解决什么问题?
- 模板的声明和定义为什么不能分开写,要想分开写该怎么做
- 模板特化
- 全特化
- 偏特化
- 模板在编译时生成的代码是否会相同,生成的相同的代码如何处理
- C++ 类对象的初始化顺序
- STL
- 容器
- 顺序型容器
- vector
- 第二个模板形参?
- vector调用resize的时候,如果是元素是一个类,会不会调用这些函数的析构函数?
- 使用Vector需要注意什么?
- 如果扩容时会引发自定义类型挨个复制构造,C++有什么机制来避免这一点
- deque
- list
- 关联式容器
- set
- map
- 红黑树的性质,各种操作时间复杂度
- unordered_map
- 哈希表跟红黑树的比较,优缺点、适用场合,各种操作的时空复杂度
- 空间配置器
- 定义
- 背景
- 实现
- 迭代器
- 迭代器用过吗?什么时候会失效?
- 迭代器的作用
- 迭代器相较于指针
- 说说 STL 中 resize 和 reserve 的区别
- resize:
- reserve:
- STL 容器动态链接可能产生的问题
- push_back 和 emplace_back 的区别
- STL 中 vector 与 list 具体是怎么实现的?常见操作的时间复杂度是多少?
- 新特性
- 智能指针
- share_ptr
- unique_ptr
- weak_ptr
- 怎么知道weak_ptr失效了没
- lambda表达式
- lambda语法:
- [capture]含义
- (parameters)含义
- mutable 或 exception 声明
- ->return-type->
- {statement}{函数体}
- Lambda表达式如何对应到函数对象
- 圆括号传参数是如何实现的
- 方括号捕获外部变量(闭包)是如何实现的
- 右值引用
- 左值的概念
- 右值的概念
- 右值引用的使用
- 左值引用的概念
- 右值引用的概念
- 移动语义
- 如何将左值强制转换为右值?
- 移动构造函数和拷贝构造函数的区别
- 转发和完美转发
- 常规转发
- 完美转发
- auto关键字,lambda表达式,nullptr,成员初始化列表
- static关键字
- 面向过程
- 面向对象
- 初始化
- C++编译过程
- 动态链接和静态链接
- 内联函数
- 定义
- 意义
- 哪些不适合作为内联函数
- 使用内联的缺点
- 和宏的区别
- 程序启动的过程
- 多态
- 静态多态
- 动态多态
- 虚函数
- 动态绑定
- 多态的好处
- 多态的形式
- 杂项
- C++内存分区(五)
- 32位整型在大小端的区别 (0x12345678)
- 内存对齐
- 内存对齐的原因
- 什么时候不应该内存对齐?
- 内存对齐的规则
- 一个空类的大小是几字节?
- 指针和引用的区别
- 浅拷贝和深拷贝的区别?
- struct和class的区别
- 导入C函数的关键字是什么,C++编译时和C有什么不同?
- 函数指针
- new和malloc
- delete如何知道该释放多大的空间,这些信息存在什么位置
- delete[]和delete的区别,基本数据类型的数组使用delete可以释放完全吗
- 堆和栈的区别
- 内存泄漏
- 说说C++的重载和重写是如何实现的
- 重载
- 重写
- 说说 C 语言如何实现 C++ 语言中的重载
- 简述下向上转型和向下转型
- 子类转换为父类
- 父类转换为子类
- 请问构造函数中的能不能调用虚方法
- 那么析构函数中能不能调用虚方法
- 请问拷贝构造函数的参数是什么传递方式,为什么
- 仿函数
- C++中类模板和模板类的区别
- 64位系统存一个地址多大空间
- 函数传递时会不会在内存拷贝
- 为什么要使用友元?
- 检查内存泄漏的方法
- C++编译和C编译的区别
- 如何判断一段函数是C++编译的还是C编译的
- 如何在不用sizeof的情况下判断系统是多少位
- 重复多次 fclose 一个打开过一次的 FILE *fp 指针会有什么结果,并请解释
- 为什么函数传递数组参数,结果数组会被修改,而值不行?
- main 函数执行以前,还会执行什么代码?
- 字符指针、浮点数指针、以及函数指针这三种类型的变量哪个占用的内存最大?为什么?
- C++几个基本类型占用空间
- 继承时应该要写哪些类的成员函数
- 怎样让对象只能创建在栈/堆/内存池中
- 只允许在栈上创建对象:
- 只允许在堆上创建对象:
- 只允许在内存池中创建对象:
- RTTI原理,type_info信息存在虚函数表的哪里
- C++在哪些情况下会产生临时对象
- C++静态链接库(lib)和动态链接库(dll)的区别
- memory_move和memory_copy是什么,他们的区别?
const
说说const int *a, int const *a, const int a, int *const a, const int *const a分别是什么,有什么特点。
- const int *a==int const *a:可以通过 a 访问整数值,但不能通过 a 修改该整数的值,指针本身是可变的,可以指向不同的整数
- const int a:a变量变成常量,不可修改
- int *const a:a的值可以更改,但是指向它的指针不能更改
- int const *const a:a本身和指向它的指针都不能更改
这里其实最重要的是第一点和第三点的区别,为了记忆方便,建议只记int * const a这种,只要在印象中不是这种写法,那么一定是第一种写法
(图片来源网络,侵删)const成员函数
- 常函数内不能修改成员变量
- 对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。
我们来看一下一个简单的示例代码,这里使用成员初始化列表为a赋初值,可以发现在常函数f中直接修改a会提示编译失败,这时我们把a修改成const int 类型,编译成功,但是仅仅是编译成功,下面输出依旧输出10
#include class A { public: int a; A() :a(10) {} void f() const{ a = 10;//编译失败:提示表达式必须是可修改的左值 const int a = 15; } } int main() { A a; a.f(); std::cout public: mutable int a; A() :a(10) {} void f() const{ // a = 10; a = 15;//编译成功 } }; int main() { A a; a.f(); std::cout T temp = a; a = b; b = temp; } T temp = a; a = b; b = temp; } // explicit instantiation for int and double types template void swap自身构造函数
(当一个类的成员是另一个类的对象时,这个对象就叫成员对象.)
STL
容器
顺序型容器
vector
特点:
- 顺序序列
- 动态数组
- 尾删有较佳性能
第二个模板形参?
vector 的第二个模板形参是分配器(allocator),用于分配和管理 vector 内部存储元素的内存。分配器可以控制内存分配的策略,例如内存池等。如果不指定分配器,默认使用 std::allocator。
分配器通常是一个模板类,提供了 allocate 和 deallocate 等成员函数来分配和释放内存。在 vector 内部,使用分配器来分配和释放存储元素的内存,可以方便地替换默认的内存分配器,实现自定义的内存分配策略。
vector调用resize的时候,如果是元素是一个类,会不会调用这些函数的析构函数?
如果调用resize函数使得vector的大小变小了,那么后面的元素会被析构掉,也就是会调用元素类的析构函数。如果调用resize函数使得vector的大小变大了,那么新添加的元素会调用元素类的默认构造函数进行初始化,而不会调用析构函数。
使用Vector需要注意什么?
- 为避免频繁的扩容操作,可以使用 reserve() 方法在插入元素之前预留一定的空间,以提高性能
- 在使用 vector 进行大量元素操作时,可以使用 emplace_back() 方法而不是 push_back() 方法,以避免元素拷贝的开销
- 在需要删除元素时,可以使用 erase() 方法进行删除。但是,需要注意的是,如果要删除多个元素,应该首先对要删除的元素进行排序,并使用 erase() 方法一次性删除,以避免多次扩容操作
如果扩容时会引发自定义类型挨个复制构造,C++有什么机制来避免这一点
在进行 vector 扩容时,如果存储的是自定义类型,会挨个复制构造元素,可能会造成性能问题。为了避免这一点,可以使用移动语义来优化。
在 C++11 引入的移动语义中,我们可以通过 std::move() 函数将一个对象转化为右值引用,这样就可以在元素的拷贝构造函数中实现移动语义,将对象的资源所有权从一个对象转移到另一个对象中,而不是进行深拷贝。
deque
特性:
- 双向队列
- 在两端增删元素有较佳性能
list
特性:
- 双向链表
- 不支持随机存取
关联式容器
set
特性:
- 不允许相同元素
- 自动排序
- 原理:红黑树
map
- first和second,并且根据first排序
- 实现原理:红黑树
- map不允许容器中有重复的key值元素
红黑树的性质,各种操作时间复杂度
自动排序,稳定
查找,插入,删除都是O(logn)
unordered_map
umap底层是哈希表
哈希表跟红黑树的比较,优缺点、适用场合,各种操作的时空复杂度
哈希表适合小数据,查找插入删除最好都是O(1),最坏O(n),缺点是容易发生哈希冲突,设计哈希函数也比较困难
红黑树适合大数据集,但是代码实现较为复杂
空间配置器
定义
在C++ STL中,空间配置器便是用来实现内存空间(一般是内存,也可以是硬盘等空间)分配的工具,他与容器联系紧密,每一种容器的空间分配都是通过空间分配器alloctor实现的。
背景
开辟内存一般分为两步,一步是用构造函数,一部分用malloc或者new,前者直接在函数调用栈开辟空间,而后者先在堆里开辟空间,再隐式调用构造函数
实现
关于内存空间的配置与释放,SGI STL采用了两级配置器:一级配置器主要是考虑大块内存空间,利用malloc和free实现;二级配置器主要是考虑小块内存空间而设计的(为了最大化解决内存碎片问题,进而提升效率),采用链表free_list来维护内存池(memory pool),free_list通过union结构实现,空闲的内存块互相挂接在一块,内存块一旦被使用,则被从链表中剔除,易于维护。
迭代器
迭代器用过吗?什么时候会失效?
顺序容器使用删除会使后面的迭代器失效(自动往前进一,导致地址全变,所以会失效),解决办法:it=earse(it),即返回删除元素下一个的迭代器
关联容器map由于内部是红黑树,使用erase不会失效,但是需要记录一下下一个元素的迭代器,list使用上面两种方法都行
迭代器的作用
和指针的区别:
迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,重载了指针的一些操作符,–>、++、–等。迭代器封装了指针,是一个”可遍历STL( Standard Template Library)容器内全部或部分元素”的对象,本质是封装了原生指针,是指针概念的一种提升,提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,–等操作。
迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用取值后的值而不能直接输出其自身
迭代器相较于指针
迭代器相对于指针的优点在于,它提供了一些安全性和抽象性的保证。例如,如果你使用一个指向数组元素的指针,你可以对它进行任何操作,包括越界访问和非法修改等操作,这可能会导致内存错误和程序崩溃。而如果你使用一个vector迭代器,则可以避免这些问题,因为迭代器会自动检查越界和非法操作,并在出错时抛出异常或者进行其他处理。
说说 STL 中 resize 和 reserve 的区别
介绍概念:capacity:该值在容器初始化时赋值,指的是容器能够容纳的最大的元素的个数。还不能通过下标等访问,因为此时容器中还没有创建任何对象。
size指的是此时容器中实际的元素个数。可以通过下标访问0-(size-1)范围内的对象。
resize:
resize即修改capacity大小,也修改size大小
reserve:
reserve只修改capcaity大小
resize既分配了空间,也创建了对象;reserve表示容器预留空间,但并不是真正的创建对象,需要通过insert()或push_back()等创建对象。
STL 容器动态链接可能产生的问题
给动态库函数传递容器的对象本身,则会出现内存堆栈破坏的问题。
产生问题的原因,容器和动态链接库相互支持不够好,动态链接库函数中使用容器时,参数中只能传递容器的引用,并且要保证容器的大小不能超出初始大小,否则导致容器自动重新分配,就会出现内存堆栈破坏问题。
push_back 和 emplace_back 的区别
如果要将一个临时变量push到容器的末尾,push_back()需要先构造临时对象,再将这个对象拷贝到容器的末尾,而emplace_back()则直接在容器的末尾构造对象,这样就省去了拷贝的过程。
STL 中 vector 与 list 具体是怎么实现的?常见操作的时间复杂度是多少?
vector:开辟三倍内存,旧数据开辟到新内存,释放旧的内存,指向新内存
新特性
智能指针
share_ptr
std::shared_ptr是一种共享式智能指针,它可以让多个shared_ptr实例同时拥有同一个内存资源。shared_ptr内部维护了一个计数器,记录当前有多少个shared_ptr实例共享同一块内存。只有当计数器变为0时,才会自动释放内存。因此,shared_ptr可以避免多个指针指向同一块内存时出现的内存泄漏和悬空指针等问题。
unique_ptr
std::unique_ptr是一种独占式智能指针,它可以保证指向的内存只被一个unique_ptr实例所拥有。当unique_ptr被销毁时,它所拥有的内存也会被自动释放。unique_ptr还支持移动语义,因此可以通过std::move来转移拥有权。
使用release()方法来移交指向的对象
weak_ptr
用来解决shared_prt相互引用冲突的结果
举个一个不太恰当的例子,A和B相互加了微信,假设我们用一个指针来指向自己的微信朋友,如果是shared_ptr,那么A和B的生命周期是相互影响的,而实际上我们并不希望这种强绑定,比如假设B注销了账户,A根本不用知道,只有当A想发消息给B的时候系统才会发出提示:您还不是该用户的朋友。这时候weak_ptr就派上用场了。这也就是weak_ptr的第一种使用场景:
当你想使用对象,但是并不想管理对象,并且在需要使用对象时可以判断对象是否还存在
- 解决循环引用:当两个或多个对象相互持有对方的 shared_ptr,会形成循环引用,导致对象无法释放。通过将其中一个对象的指针设置为 weak_ptr,而不是 shared_ptr,可以打破循环引用,避免内存泄漏。
- 安全地访问对象:在需要访问被 shared_ptr 管理的对象时,可以通过 weak_ptr 的 lock() 方法尝试转换为 shared_ptr,如果对象仍然存在,则返回一个有效的 shared_ptr,否则返回一个空指针。
- 提高性能:weak_ptr 不会增加对象的引用计数,因此不会影响对象的生命周期,也不会阻止对象的销毁。这样可以更灵活地管理对象的生命周期,提高程序性能。
怎么知道weak_ptr失效了没
可以通过expired()函数来判断一个weak_ptr是否已经失效,如果expired()返回true,则表示它指向的对象已经被销毁或释放了。另外,使用lock()函数获取weak_ptr指向的对象时,如果返回的是一个空的shared_ptr,也可以判断weak_ptr是否已经失效。
lambda表达式
lambda语法:
[capture] (parameters) mutable ->return-type{statement}
[capture]含义
- []。没有任何函数对象参数。
- [=]。函数体内可以使用 Lambda 所在范围内所有可见的局部变量(包括 Lambda 所在类的
this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)。
- [&]。函数体内可以使用 Lambda 所在范围内所有可见的局部变量(包括 Lambda 所在类的
this),并且是引用传递方式(相当于是编译器自动为我们按引用传递了所有局部变量)。
- [this]。函数体内可以使用 Lambda 所在类中的成员变量。
- [a]。将 a 按值进行传递。按值进行传递时,函数体内不能修改传递进来的 a 的拷贝,因为默认情况下函数是 const
的,要修改传递进来的拷贝,可以添加 mutable 修饰符。
- [&a]。将 a 按引用进行传递。
中括号 “[]” 表示Lambda表达式的捕获列表,用于指定Lambda表达式访问外部作用域中的变量的方式。捕获列表可以为空,或者包含一个或多个捕获项,
int a = 1; auto lambda = [a](int x, int y) -> int { return a + x + y; };
在这个例子中,捕获列表包含一个捕获项 “a”,表示Lambda表达式将访问外部作用域中的变量 “a”。
(parameters)含义
标识重载的 () 操作符的参数,没有参数时,这部分可以省略。参数可以通过按值(如: (a, b))和按引用 (如: (&a, &b))
两种方式进行传递。
[](int x, int y) -> int { return x + y; }
圆括号 “()” 表示Lambda表达式的参数列表,可以包含零个或多个参数。在这个例子中,Lambda表达式有两个参数,分别是一个整数 “x” 和一个整数 “y”
mutable 或 exception 声明
这部分可以省略。按值传递函数对象参数时,加上 mutable
修饰符后,可以修改传递进来的拷贝(注意是能修改拷贝,而不是值本身)。exception
声明用于指定函数抛出的异常,如抛出整数类型的异常,可以使用 throw(int)。
->return-type->
返回值类型:标识函数返回值的类型,当返回值为 void,或者函数体中只有一处 return
的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略
{statement}{函数体}
标识函数的实现,这部分不能省略,但函数体可以为空。
Lambda表达式如何对应到函数对象
当定义一个Lambda表达式时,编译器会生成一个与Lambda表达式对应的新的(未命名的)函数对象类型和该类型的一个对象。这个函数对象可以重载函数调用运算符(),从而具有类似函数的行为。
圆括号传参数是如何实现的
圆括号传参数是通过函数调用运算符()来实现的。
当你使用圆括号传递参数给一个lambda表达式时,实际上是调用了它生成的函数对象的函数调用运算符(),并将参数传递给它。
函数调用运算符()会根据lambda表达式的定义来执行相应的代码,并返回一个值(如果有的话)。
所以,你可以把圆括号传参数看作是一种调用函数对象的方式,它让你不需要知道函数对象的名字或者类型就可以使用它。
方括号捕获外部变量(闭包)是如何实现的
方括号捕获外部变量(闭包)是通过将外部变量作为函数对象的成员来实现的。
当你在方括号中指定一个外部变量时,编译器会为你生成一个函数对象类型,它包含了这个外部变量作为它的一个成员。
当你创建一个函数对象时,这个成员会被初始化为外部变量的值或者引用,这取决于你是用=还是&来捕获它。
当你调用函数对象时,这个成员就可以在lambda表达式中使用,就像一个普通的局部变量一样。
所以,你可以把方括号捕获外部变量看作是一种创建闭包的语法糖,它让你不需要显式地定义一个类或者接口来保存外部变量的状态。
- 值捕获(capture by value):使用 “=”
将外部变量按值进行捕获。Lambda表达式会在创建时将外部变量的值复制一份到闭包中
- 引用捕获(capture by reference):使用 “&”
将外部变量按引用进行捕获。Lambda表达式会在创建时绑定到外部变量的内存地址,以便在Lambda表达式中修改变量的值
- 隐式捕获:使用 “[]” 作为空方括号,表示隐式捕获所有在Lambda表达式中使用的外部变量。在Lambda表达式中使用的变量会被自动按值进行捕获。
int x = 3; auto lambda = [&] { return x * x; }; int result = lambda(); // result = 9
需要注意的是,对于值捕获和隐式捕获,Lambda表达式在创建时会复制一份外部变量的值到闭包中,如果在Lambda表达式中修改闭包中的变量值,不会影响外部变量的值。而对于引用捕获,Lambda表达式会直接操作外部变量,可以改变其值。
右值引用
右值引用是C++11引入的一种引用类型,它用于表示临时对象和即将销毁的对象
在了解右值之前,我们先来了解一下左值
左值的概念
左值是指可以出现在赋值运算符左边的表达式,也就是具有内存地址且可被取地址的表达式。通常来说,变量、对象以及可以引用的表达式都是左值。左值表示的是一个具体的内存位置,可以对其进行读取和写入操作
int x = 10; // x 是一个左值 int y = 15; int &ref = y; // ref 是一个左值
看完了左值之后我们来看右值
右值的概念
右值是指不能出现在赋值运算符左边的表达式,通常是临时性的、不具有明确内存地址的值。字面常量、临时对象、函数返回值等都属于右值。右值表示的是一个临时的值,不能被取地址。
int x=10; //在刚才的例子中,x是左值,而10就是右值 std::string greeting = std::string("Hello, World!"); // std::string("Hello, World!") 是一个右值 int val = getValue(); // getValue() 的返回值是一个右值
右值引用的使用
和刚才的套路一样,为了研究右值引用是什么,我们先来研究左值引用
左值引用的概念
左值引用是 C++ 中最常见的引用类型,用于绑定到左值表达式上。左值引用通过 & 符号表示。
可以说左值引用就是我们平时常用的“引用”
void processObject(Object& obj) { // 对传入的对象进行处理 } Object largeObject; processObject(largeObject); //用于传递可修改的参数;用于避免拷贝开销
右值引用的概念
右值引用是 C++11 引入的一个重要特性,用于绑定到临时对象或右值表达式上,以支持移动语义和完美转发。右值引用通过 && 符号表示。
int num = 10; //int && a = num; //右值引用不能初始化为左值 int && a = 10;
和常量左值引用不同的是,右值引用还可以对右值进行修改。例如:
int&& a = 10; a = 100; std::cout 1, 2, 3}; std::vector std::cout func(arg); // 尝试将通用引用参数传递给接受右值引用的函数,会导致编译错误 } int main() { int value = 42; wrapper(std::move(value)); // 将左值转换为右值传递给 wrapper return 0; } std::cout func(std::forward int value = 42; wrapper(value); // 正确地将左值参数传递给 wrapper,然后再传递给 func return 0; } a++; } int main() { printf("a = %d\n", a);//输出:10 Func();//输出:10 printf("a = %d\n", a);//输出:11 system("pause"); return 0; } static int a = 5; printf("a = %d\n", a); a++; } int main() { for (int i = 0; i
- 值捕获(capture by value):使用 “=”
还没有评论,来说两句吧...