本文内容是学习C++/C这么久以来的针对一些问题经验汇总,分三个模块陈述,会在后续的学习过程中不断更新(因为还有一些没汇总成文字);
注意事项
1、char与int之间的类型转换,直接转换是不行的,转换的是字符的ASCII码。
- 这个问题应该是比较显然的;
2、赋值运算的优先级较低,而赋值语句会经常出现在条件当中。因赋值运算的优先级相对较低,所以通常需要给赋值部分加上括号使其符合我们的原意!
3、除非必须,否则不用递增递减运算符的后置版本!
4、位移时关于符号位如何处理并没有明确的规定,所以强烈建议仅将位运算符用于处理无符号类型。
5、强烈建议通过引用形参访问该类型的对象,如果不设计到对象的写操作,或者说无须改变引用形参的值,则尽可能把形参定义成对常量的引用。
6、不要返回局部对象的引用或指针,因为函数完成后,它所占用的存储空间也随之被释放掉,因此,函数终止意味着局部变量的引用将指向不再有效的内存区域。
7、所谓运算符优先级,先要理解什么是运算符,以一句代码为例:auto val = *(vec.begin());像在这一行代码中,*与.就是运算符,但是.优先级高于*,因此需要加上括号,同时我们也可以换种方式处理实现相同的目的auto val = vec->begin();(只是举个例子,vec不能用->)。
8、如果在函数中我们有返回数组的需求,但因为函数无法返回多个变量,因此我们可以让其返回一个指向包含多个整数的数组的指针。
9、放在main函数之外的变量属于全局变量,任何函数可以调用,想到之前的一个疑惑,特此记录。
10、一般来说,在类中不建议使用其他成员的名字作为某个成员函数的参数,同时也不建议隐藏外层作用域中可能被用到的名字。
11、怎么理解C++中的某些信息在编译时知晓,在运行时知晓?
- 最明显的就是类的继承体系中关于动态绑定的那部分,分为静态类型与动态类型两种情况;
- 静态类型在编译期就知道具体类型,根据相应函数调用相应方法;
- 动态类型,比如说基类的指针或引用,只有在运行时才知道具体调用哪个版本的方法;
12、在C++中,返回对象的引用而不是对象自身有几个原因:
- 效率更高,返回对象引用无需产生对象的拷贝,可以避免额外的对象拷贝带来的时间和空间开销;
- 可以对返回的引用执行修改操作,如果返回对象自身,则调用者得到的是一个副本,无法修改原对象;
- 还有一个说是为了避免对象切割,这个不是很理解;
13、在运算符重载这部分,+=定义在类内,+定义在类外,为什么要这么设计?
+=并不产生新的对象,它在原对象基础上修改,在类内可以直接操作原对象,返回对象自身的引用;- 而
+需要处理两个对象,在类内只能顾到该对象自身,而设计在类外可以实现对多个对象的处理,且构造一个全新的对象返回结果;
14、在链表练习中遇到的一些指针使用问题,对基础进行进一步加强
- 给定一个指针
p1,我再将该指针的值赋值给另一个指针p2; - 更改
p1的值不会影响p2的值,但是如果更改的是指针p1指向的内容的值; - 那
p2指向的那片空间的内容自然也发生改变了;
15、main函数处理命令行选项:
比如我们编译了这么一个程序:prog
这个程序会附带一些命令选项,比如我们执行:prog -d -o ofile data0
这些命令行通过两个(可选的)形参传递给main函数:int main(int argc, char *argv[])
现在上述的这些命令选项我们想要将他们连接成一个字符串(string),并输出!示例代码:
1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <string>
int main (int argc, char *argv[])
{
std::string str;
for (auto i = 1; i < argc; ++i)
str = str + argv[i] + " ";
std::cout << str << std::endl;
return 0;
}
这样就实现了我们的目的!
16、在main函数执行前先运行的的函数
- 使用
gcc拓展,告知编译器这个函数应当在main函数执行之前执行,与此同时也可以告知编译器应当main函数执行之后,程序结束之前运行; - 通过调用全局
static变量去执行函数; - 还有一种就是利用
lambda表达式执行;
17、学习到的C++ 11新特性
- 定义了空指针
nullptr,替代NULL(具体原因网上有分析) - 新增了Lambda表达式;
- 新增了右值引用,这在C++ 11标准以前是不被允许的;
- 新增了泛化的常量表达式-主要是
constexpr; - 新增了初始化列表,更好的简洁了语法,其次也统一了初始化语法;
- 新增了类型推导-体现在关键字
auto上,同时使用decltype推导表达式类型,具体规则参考本文笔记; - 新增了基于范围的
for循环; - 新增了委托构造函数,构造函数可以调用另一个来实现代码复用;
- 在类的继承部分增加了
final和override关键字,前者限制继续继承,后者关键字表明这是对虚函数的重写,语义更清晰; - 增加了显式指定或禁止编译器的默认行为的功能;
- 增加了静态
assertion,原本只有两种方式来assert,一种是assert宏,在运行期起作用,一种是预处理指令#error,在预处理期起作用,而新增的静态assertion可以在编译期就能生效,这样针对模板等工具有更好的处理; - 增加了智能指针,极大方便了动态空间的管理;
- 增加了正则表达式的匹配,这是一个跨时代的变化;
- 新增了增强的元组
tuple,相比pair可以更好的支持变长参数; - 新增哈希表,C++11标准以前,
map、multimap、set、multiset使用红黑树实现,插入和查询的复杂度是log(n);- 新标准新增了,或者说额外拓展了四种模板类:
unordered_set、unordered_multiset、unordered_map、unordered_multimap;
- 新标准新增了,或者说额外拓展了四种模板类:
报错情形
1、提示表达式必须包含指向对象的指针类型 或者 提示`int[int]`用作数组下标类型无效
发现的原因:代码中同时定义了一个int型的变量a,但同时以该变量做了数组名,所以发生了冲突……
2、警告:在有返回值的函数中,控制流程到达函数尾
缺少return,主要是在遇到if条件之外没有返回值的情况,所以会报警告。
3、reference to non-static member function must be called
这个问题是指你引用(调用)了非静态函数,但你不是通过类对象来调用的。
问题的来源就是sort()函数的第三个谓词参数:
按照常理来说,同一个类的非静态const成员函数中能相互调用,而不用通过类对象进行访问,为什么这里不行呢?
问题的原因其实就是函数参数不匹配的问题。因为我们普通的成员函数都有一个隐含的this指针,表面上看我们的谓词函数只有两个参数,但实际上它有三个参数(多了一个this),而我们调用sort()排序函数的时候只需要用到两个参数进行比较,所以就出现了形参与实参不匹配的情况。
所以,解决办法就是把谓词函数com()定义为static成员函数,这样就不存在对this指针的需求了。
低级错误
1、是因为在写代码的过程中发现,printf在输出时末尾总会带一个百分号%,后面加了换行符\n发现解决了问题。
2、在C语言中,求幂不能直接用^符号,在C语言中这是按位异或运算符。
3、在循环遍历数组或者说字符串的时候,我们要记得要控制索引下标在字符串长度范围内,一定要防止出现越界错误。
4、在定义vector变量后,进行初始化之后,如果我们进行push_back会直接在初始化变量的后面添加数据,所以尽可能不用多此一举,或者换用下标的形式。
秋招八股汇总
编译链接
动态链接与静态链接
静态链接是将所有的库文件和程序代码在编译时合并为一个单独的可执行文件的过程。程序运行时,操作系统加载并执行这个完整的可执行文件。
优点:
- 可执行文件独立,不依赖于外部的库文件,可以在没有安装对应库文件的系统上运行。
- 静态链接可以提供更好的性能,因为所有代码和数据都在同一个文件中,避免了运行时的库加载和链接过程。
缺点:
- 可执行文件的大小较大,因为包含了所有的库文件代码和数据,导致占用更多的磁盘空间。
- 静态链接的可执行文件无法共享库所带来的更新和优化。
动态链接将程序与所需的库文件分开存储,在动态链接中,编译器只将程序代码与外部库的引用信息包含在可执行文件中,而将实际的库文件保存为独立的动态链接库(DLL或共享对象),当程序运行时,操作系统会在需要的时候加载这些动态链接库。
优点:
- 可执行文件较小,只包含程序代码和对库的引用信息,因此占用较少的磁盘空间。
- 多个程序可以共享同一个动态链接库,节省内存空间。
- 动态链接库的更新和优化可以直接影响使用该库的多个程序,无需重新编译整个程序。
缺点:
- 程序在运行之前需要确保所需的动态链接库已经安装在系统中,如果库文件缺失或版本不匹配,程序可能无法正常运行。
- 动态链接可能会引入一些运行时的开销,包括加载和链接库文件的时间。
静态变量的初始化
- 在
C语言中未初始化的静态变量会初始化为0; C语言中静态变量的初始化发生在任何代码执行之前;CPP由于引入了对象的构造,因此在未初始化的情况下会调用对象的默认构造函数;CPP则是当且仅当对象首次用到才进行构造;
内存管理
内存分配
new与mallocnew属于操作符,操作符是一种特殊的语法元素,用于对操作数进行操作或执行特定的操作,而malloc为内存分配函数;new作为操作符不可以重载,但是new相关的函数是可以重载的,而malloc本身不可重载;new失败时抛出bad_alloc异常,malloc在分配失败时返回NULL指针;malloc大抵上分为以下几个步骤:接收内存块的分配请求、寻找合适的内存块、分配内存块、返回指针;
内存泄漏
产生原因:资源未得到合理的释放,一般会伴随着忘记delete,或者在继承体系中析构函数未设置为virtual,智能指针中的循环引用计数出现问题,从而导致资源释放出现问题;
悬空指针和野指针
悬空指针是指指向已释放或无效的内存地址的指针。当使用指针访问已释放的内存或无效的内存时,可能会导致未定义的行为。悬空指针通常是由以下情况引起的:
- 释放内存后未将指针置空:在使用
delete释放内存后,如果没有将指针置为nullptr,指针仍然保留之前的内存地址,成为悬空指针。 - 返回局部变量的指针:当函数返回一个指向局部变量的指针时,因为局部变量的生命周期限制在函数的作用域内,在函数结束后,该指针将成为悬空指针。
- 对象销毁后仍然持有指向其成员的指针:如果对象被销毁,而指向其成员的指针仍然存在,并且尝试访问指针指向的成员,将导致悬空指针。
- 释放内存后未将指针置空:在使用
野指针是指指向未知或未分配的内存地址的指针。野指针通常是由以下情况引起的:
- 指针未初始化:声明指针但未将其初始化,导致指针的值是未知的,成为野指针。
- 使用已释放的指针:当使用已释放的指针(如delete或free后的指针)时,该指针成为野指针。
- 越界访问数组:如果指针超出了数组的边界范围,指向的内存地址是未知或未分配的,也形成了野指针。
内存对齐
在C语言中,内存对齐是指数据在内存中的存储位置满足特定的对齐要求。对齐要求是由编译器和硬件平台决定的,并且可以通过编译器指令或编译选项进行设置。
内存对齐的目的是提高内存访问的效率,因为许多计算机体系结构对未对齐的数据访问会导致性能损失或错误。
C语言中可以使用以下两种方式来设置内存对齐:
结构体成员对齐: 在结构体定义中,可以使用特定的对齐指令来设置结构体成员的对齐方式。常用的对齐指令有#pragma pack和__attribute__((packed))。
#pragma pack(n)指令将结构体成员的对齐方式设置为n字节,通常情况下n为2的幂(如1、2、4、8等)。这样可以确保结构体成员按照指定的字节对齐。__attribute__((packed))属性可以用于告诉编译器取消对齐,使得结构体成员按照最小对齐方式进行布局。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#pragma pack(4) // 按照4字节对齐
struct MyStruct {
int a;
char b;
float c;
};
// 或者
struct __attribute__((packed)) MyStruct {
int a;
char b;
float c;
};
同时一些编译器提供了选项来设置默认的内存对齐方式。例如,GCC编译器可以使用-malign-typebound选项来设置默认的对齐方式。
使用编译器选项设置的对齐方式将适用于所有的数据类型和结构体。
需要注意的是,在设置内存对齐时需要权衡内存使用和性能。较小的对齐值可以节省内存,但可能会导致性能下降。较大的对齐值可以提高性能,但会增加内存消耗。根据具体的需求和平台要求,选择合适的内存对齐设置。
对象分配
如何使得程序只能分配在堆上?
- 使用工厂模式,设计一个类,禁用其默认构造函数和复制构造函数,且将构造函数设计为私有权限,只允许通过静态成员函数或工厂函数来创建对象。这样,只能通过特定的方式(如静态成员函数或工厂函数)来动态分配对象,而无法在栈上直接声明对象。
面向对象
多态性
- 底层使用了虚函数表,虚函数表存放的内容在编译期间写入,在构造函数执行之后再初始化,虚函数表指针是存在于对象之内的,因此一般计算大小时要加上这部分内容;
- 一般会通过
override关键字表明这是对一个虚函数的重写,当然在这种场景下没有该关键字也是可以正常实现重写的功能,但强烈建议写上;
向上转型和向下转型
- 向上转型指的是将派生类对象的实际地址用基类指针指向,这是多态性实现的关键环节,而且可以认为是安全的,因为在调用基类指针时我们只可能调用到基类指针中所含有的方法;
- 向下转型则是将基类对象的实际地址用派生类指针指向,这是不安全的,因为我们通过派生类指针调用到基类中并不存在的那个方法时,会报错;
- 如果是通过
dynamic_cast进行转换,那么失败时会返回空指针nullptr; - 如果使用
static_cast进行向下转型,编译器会执行一种静态转换,而无法在运行时检查类型是否匹配,在这种情况下,会发生未定义的行为,这可能导致程序崩溃、数据损坏或其他不可预测的结果。
- 如果是通过
标准库
几种容器
顺序容器
顺序容器主要包括vector、list、deque三大类型;
在vector中,存在着resize与reserve两种函数;
resize:用于调整容器的大小,可以增加或者减少容器中元素的数量,增加时新添加的元素会被默认初始化,减小时超出大小的元素会被删除;reserve:用于预留容器的内存空间,即分配足够的内存以容纳指定数量的元素,但并不创建元素;它仅影响容器的容量,不会改变容器的大小。当预先知道容器可能存储的元素数量时,可以使用reserve()函数来避免多次重新分配内存的开销,提高性能;
关联容器
关联容器主要包括set、map、unordered_set、unordered_map三大类型;
容器适配器
容器适配器主要包括priority_queue、stack、queue三大类型;
C++11新特性
- 针对空指针增加了
nullptr的常量; - 新增关键字
decltype用于类型推导,以在编译时获取表达式的类型; - 新增了
lambda匿名函数表达式; - 新增了智能指针标准库
shared_ptr- 构造对象注意事项:使用两个裸指针构造两个
shared_ptr会导致内存泄漏问题,因为shared_ptr由于它RAII的设计原则,会在一段作用域之后自动释放资源,那么当再次使用该裸指针就会出现问题; - 建议使用
make_shared函数进行shared_ptr的构造,其一可以避免上述问题,其二可以省去一部分构造的过程; - 当一个
shared_ptr赋值给另一个shared_ptr时,两个只能指针共享对象所有权;
- 构造对象注意事项:使用两个裸指针构造两个
unique_ptr- 一个
unique_ptr赋值给另一个unique_ptr时,会发生所有权的转移 ,但需要注意的一个细节是:转移所有权需要使用move,这是标准库层面的要求;
- 一个
weak_ptr- 一般来说,不对
weak_ptr解引用,因为它无法保证指向的对象存在; - 安全的用法是通过
lock函数转成shared_ptr,指向的对象存在,会将之转成shared_ptr,否则指向空;
- 一般来说,不对
智能指针的写入并非线程安全,通过原子操作解决这一问题,一是通过硬件级别的支持,二则是需要操作系统层面的支持;
模板
类模板/函数模板
类模板与函数模板可以实现对多种类型的统一化,我们不需要针对某一种类型去专门设计一个类或者一个函数;
模板类
模板类是通过类模板生成的具体类的实例,即特例化后的类;
右值引用与完美转发
右值引用的提出,主要是为了移动语义而准备,完美转发的目的主要是为了是参数引用类型保持不变;