废弃,请参考
小问题
虚函数表的存储位置?
声明和定义之间的区别
string字符串具体数据的具体存储位置
数组的成员函数初始化
1 | A bbb[2]; // no! |
在阅读牛客网的资料的过程中发现的操作系统相关的问题
进程与线程之间的差异,通讯的方法(线程、进程)、虚拟内存的管理手段(对应方式)、缺页中断,缺页置换算法,页表寻址等,等等硬链接与软链接、http&https TCP保证可靠性的方法 redis相关知识点 锁的分类 线程模型 协程 系统调用相关 用户态到内核态的转变过程
数据结构相关:红黑树、AVL树、B+树 tcpudp适用场景 阻塞,非阻塞,同步,异步
hash冲突 数据库wal技术 洗牌算法 分布式缓存和分布式存储的设计
手写快排、以及各种排序算法 稳定的排序:基数排序、冒泡排序、直接插入排序、折半插入排序、归并排序
fork、vfork 文件句柄数 隐式类型转换 RTTI 迭代器删除元素的问题 web服务接受请求的模型了解一下nginx高效的原因 IO模型 异步编程的事件循环
C++ Primer Plus
第9章 内存模型和名称空间
针对一个变量,从以下几个维度考虑:
- 存储位置:分为静态(持续)变量,自动变量,在堆上使用
malloc
或者new
分配出来的变量 - 生命周期:静态变量贯穿程序运行周期;自动变量随作用域进入而创建、退出而消亡;堆上靠程序员
- 可访问范围(内外部链接、无链接):仅针对静态持续变量而言,通过
exter
和static
区分 - 是否可改变(常量性):是否有const修饰
- 数据类型:基本类型、复合类型、用户自定义类型
静态持续变量
静态变量的特点是:整个程序执行期间都存在 & 分配固定的存储块进行存储 & 默认初始化为0。
与静态持续变量相对应的是自动变量,由栈进行管理,生命周期跟随代码块的进入而诞生,退出而消亡。
C++为静态存储连续性变量提供了三种链接性,如下。
外部链接性
可在其他文件访问
必须在代码块之外进行声明
C++提供了两种变量声明:
定义声明(定义),会分配内存空间。?和定义之间的区别?
全局变量的定义,应该遵循单定义规则,否则会出错。即,
使用多文件的程序,只能在一个文件(且只能在一个文件)中定义外部变量,使用该变量的其他文件必须使用external声明,并且不能初始化。引用声明(声明),使用关键字
extern
,且不进行初始化,则不会创建新的内存空间
Demo
1
2
3double up; // definition, up is 0
extern int blem; // blem defined elsewhere
extern char gr = 'z'; // definition because initialized1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// file01.cpp
extern int cats = 20; // definition because of initialization 关键字extern也可以省略
int dogs = 22; // also a definition
int fleas; // also a definition
extern int a; // 出错,无法识别谁进行了定义操作
...
// file02.cpp
// use cats and dogs from file01.cpp
extern int cats; // not definitions because they use
extern int dogs; // extern and have no initialization
extern int a; // 出错,无法识别谁进行了定义操作,和file01对应
...
// file98.cpp
// use cats, dogs, and fleas from file01.cpp
extern int cats;
extern int dogs;
extern int fleas;
...参考extern引入其他文件中的变量 support和external两个文件中的内容,编译命令如下
1
g++ external.cpp support.cpp -o support
全局变量更适合定义常量
1
2
3// 第一个const,不能通过指针修改内容;第二个const不能修改指针本身
const char * const months[12]={"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"
};
内部链接性
只能在当前文件中访问
必须在代码块之外进行声明,并使用
static
修饰符修饰通过内部链接性声明,覆盖外部全局变量。
参考variables with external and internal linkage ,编译命令如下
1
g++ twofile1.cpp twofile2.cpp -o twofile
无链接性
1 | for i in range(1): |
1 | void func(){ |
只能在当前函数或者代码块中访问
在代码块内部进行声明,并使用
static
修饰符修饰在代码块首次被执行之后,才开始存在
该变量只在该代码块中可用,但在该代码块不活跃的时候依然存在,此外初始化操作只会执行一次
参考static修饰的变量 ,使用如下命令进行编译
1
g++ static.cpp -o static
总结
声明的方式:
1 | int global = 1000; // static duration, external linkage |
五种变量的存储模式如下图所示
说明符和限定符
命名空间
传统的C++名称空间
- 声明区域:可以在其中进行声明的区域
- 潜在作用域:从声明点开始到其声明区域的结尾
- 作用域:变量对于程序可见的潜在作用域部分,全局变量可能会被局部变量隐藏(这个时候可以使用作用域解析符来使用全局变量)
如下图所示
名称空间
名称空间可以使全局的也可以在另一个名称空间中。名称空间中声明的名称的链接性为外部性
使用名称空间
- using声明:使得一个变量可用,类似于声明了
- using编译指令:using namespace,使得整个命名空间的变量可用,进行名称解析
局部变量会覆盖引入的全局变量。因此在出现同名的情况下,需要使用::
来标识全局变量
名称空间的嵌套
1
2
3
4
5
6
7
8
9namespace elements{
namespace fire{
it flame;
}
}
namespace myth{
using elements::fire::flame;
}
myth::flame;命名空间的别名
1
namespace mvft = my_very_favorite_thins;
未命名的名称空间
可以当作是内部链接性的替代方案,例如:
1
2
3
4namespace { // 限制了变量的潜在作用区域
int a;
}
static int a; // 两种表述方式是等价的,都是静态存储,内部链接
内存管理(leetcode-book)
转载自leetcode book,参照链接。
内存分区
C++内存分区:栈、堆、全局/静态存储区、常量存储区、代码区(这些区域也是每个进程控制的区域)。
栈:存放函数的局部变量、函数参数、返回地址等,由编译器自动分配和释放。
映射区:存储动态链接库等文件映射、申请大内存(malloc时调用mmap函数)??? 不理解其中的含义
堆:动态申请的内存空间,就是由 malloc 分配的内存块,由程序员控制它的分配和释放,如果程序执行结束还没有释放,操作系统会自动回收。
全局区/静态存储区(.bss 段和 .data 段):存放全局变量和静态变量,程序运行结束操作系统自动释放,在 C 语言中,未初始化的放在 .bss 段中,初始化的放在 .data 段中,C++ 中不再区分了。
常量存储区(.data 段):存放的是常量,不允许修改,程序运行结束编译器自动释放。
在实际实验过程中,baseType的常量地址小于静态变量的地址;但是string没有体现地址小于静态变量的地址
代码区(.text 段):存放代码,不允许修改,但可以执行。编译后的二进制文件存放在这里。
说明:从操作系统的本身来讲,以上存储区在内存中的分布是如下形式(从低地址到高地址):.text 段 --> .data 段 --> .bss 段 --> 堆 --> unused --> 栈 --> env
堆和栈之间的区别
- 申请方式:栈是系统自动分配,堆是程序员主动申请。
- 申请后系统响应:分配栈空间,如果剩余空间大于申请空间则分配成功,否则分配失败栈溢出;申请堆空间,堆在内存中呈现的方式类似于链表(记录空闲地址空间的链表),在链表上寻找第一个大于申请空间的节点分配给程序,将该节点从链表中删除,大多数系统中该块空间的首地址存放的是本次分配空间的大小,便于释放,将该块空间上的剩余空间再次连接在空闲链表上。
- 栈在内存中是连续的一块空间(向低地址扩展)最大容量是系统预定好的,堆在内存中的空间(向高地址扩展)是不连续的。
- 申请效率:栈是有系统自动分配,申请效率高,但程序员无法控制;堆是由程序员主动申请,效率低,使用起来方便但是容易产生碎片。
- 存放的内容:栈中存放的是局部变量,函数的参数;堆中存放的内容由程序员控制。
变量的区别
全局变量、局部变量、静态全局变量、静态局部变量的区别
C++ 变量根据定义的位置的不同的生命周期,具有不同的作用域,作用域可分为 6 种:全局作用域,局部作用域,语句作用域,类作用域,命名空间作用域和文件作用域。
从作用域看:
- 全局变量:具有全局作用域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件。当然,其他不包含全局变量定义的源文件需要用 extern 关键字再次声明这个全局变量。 静态全局变量:具有文件作用域。它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被 static 关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。
- 局部变量:具有局部作用域。它是自动对象(auto),在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用执行结束后,变量被撤销,其所占用的内存也被收回。 静态局部变量:具有局部作用域。它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。
从分配内存空间看:
- 静态存储区:全局变量,静态局部变量,静态全局变量。
- 栈:局部变量。
说明:
- 静态变量和栈变量(存储在栈中的变量)、堆变量(存储在堆中的变量)的区别:静态变量会被放在程序的静态数据存储区(.data 段)中(静态变量会自动初始化),这样可以在下一次调用的时候还可以保持原来的赋值。而栈变量或堆变量不能保证在下一次调用的时候依然保持原来的值。
- 静态变量和全局变量的区别:静态变量用 static 告知编译器,自己仅仅在变量的作用范围内可见。
对象创建限制在堆或栈
对象的建立分为两种
- 静态建立:由编译器在栈空间为对象分配空间,直接调用类的构造函数创建对象。例如:
A a;
- 动态建立:使用 new 关键字在堆空间上创建对象,底层首先调用 operator new() 函数,在堆空间上寻找合适的内存并分配;然后,调用类的构造函数创建对象。例如:A *p = new A();
- 静态建立:由编译器在栈空间为对象分配空间,直接调用类的构造函数创建对象。例如:
限制在堆
私有化构造函数
私有化析构函数
编译器发现析构函数不可访问,即不能自动完成对象的创建和释放,就不会在栈上为对象分配内存。
但无法解决继承问题限制在栈
构造函数设置为 protected,并提供一个 public 的静态函数来完成构造,而不是在类的外部使用 new 构造;将析构函数设置为 protected。原因:类似于单例模式,也保证了在派生类中能够访问析构函数。通过调用 create() 函数在堆上创建对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class A
{
protected:
A() {}
~A() {}
public:
static A *create()
{
return new A();
}
void destory()
{
delete this;
}
};将 operator new() 设置为私有。原因:当对象建立在堆上时,是采用 new 的方式进行建立,其底层会调用 operator new() 函数,因此只要对该函数加以限制,就能够防止对象建立在堆上
1
2
3
4
5
6
7
8
9class A
{
private:
void *operator new(size_t t) {} // 注意函数的第一个参数和返回值都是固定的
void operator delete(void *ptr) {} // 重载了 new 就需要重载 delete
public:
A() {}
~A() {}
};
类的大小
对齐原则
与普通变量有关
虚函数
虚继承
虚继承中如果出现钻石继承,基类只会有一个存在,此外虚拟派生类中会有一个指向基类对象的指针,这些特性都会造成类大小的计算的差异
1
2
3
4
5
6class A{};
class B: virtual public A{};
class C: virtual public A{};
class D: public B, public C{
D():A(),B(),C(){};
}; //空类
假设结构体起始位置0,字节对齐的原则:首先找最长成员 a bytes,pack参数为 b bytes,那么有效单位长度为x=min(a, b)
,然后把地址按照 x 划分,然后一个个往里面填,最后不够 x ,要补足x。
第10章 对象和类
参考深度探索CPP-对象模型,理解:
- 构造函数为什么不能被继承(为什不能是虚函数)
- 虚函数能实现多态的原因,为什么一个指向派生类的父类指针可以执行到派生类的方法
内联函数(由inline声明), 会出现在所有调用的地方,提高函数的执行效率。其他的函数方法都只会有一个代码副本,调用的时候需要跳转到该位置才能继续执行。
友元
友元函数,本质上不是类的成员函数,只是数据访问权限和类函数相同 ?看书的定义与实现,参数列表不对?
1
2
3
4
5
6
7
8
9
10class Time{
friend operator*(double m, const Time& t);
}
Time operator*(double m, const Time& t){ .. } // 在实现的时候不可以添加Time::限定符
// 可以将上述函数作为Time类的友元,来实现下述浮点数在前的问题
Time b;
Time a = 2.75 * b; //
Time c = b * 2.75; // 2.75会被隐式转换
// 在类中定义友元函数
frind Time operator*(double m, const Time& t);重载
<<
操作符1
2
3
4
5
6
7
8
9
10
11// 第一种方式
void operator<<(ostream& os, const Time& t){
os<< t.hours << " hours, " << t.minutes << " minutes";
}
// 第二种方式
ostream & operator<<(ostream& os, const Time& t){
os<< t.hours << " hours, " << t.minutes << " minutes";
return os;
}
// 采用第二种方式可以实现下述操作
cout << x << y;其中
cout << x
返回的也是一个os对象,就可以被第二<<
符号使用了
类方法
构造函数
析构函数
析构函数的执行顺序是:派生类本身的析构函数、对象成员的析构函数、基类的析构函数。在基类中记得把析构函数声明成虚函数,以保证类的析构是完整的
第13章 类继承
1 | class TableTennisPlayer { |
-1- 公有派生:基类的公有成员将变为派生类的公有成员;基类的私有部分也会成为派生类的一部分,但是只能通过基类的公有和保护方法访问。
创建派生类对象时,程序首先创建基类对象。若派生类没有调用基类的构造函数,编译器会默认调用基类的构造函数。
释放对象的顺序和创建对象的顺序相反,首先执行派生类的析构函数,然后自动调用基类的构造函数。
静态联编和动态联编
动态联编通过每个类new出来的虚拟指针vptr
指向一个该类所有需要动态联编函数的具体地址。
实例化一个对象
1 |
|
从空间的位置构造上看,并没有b的空间被开辟出来。
第14章 C++中的代码重用
包含对象成员的类
包含对象成员的类的实现方式:
- 在类中包含另一个类的对象
- 私有继承(允许多继承)
继承之间的比较
C++的继承有如下几种:
- 私有继承
- 保护继承
- 公有继承
- 虚继承(多重继承MI)
虚继承
若按照正常的继承逻辑,会造成钻石结构的继承出现下述的情况
虚继承正是为了实现我们想要的下述结构来实现,主要思想是Singer
和Waiter
在构造的时候,不会先构造基类的内容
类模板
第16章 String类和标准模板库
智能指针模板类
阅读下面C++习题中的智能指针的介绍吧,本以为书中会从源码的角度进行剖析,没想到只是简单用法上的介绍,并且没有weak_ptr的相关介绍。感谢建邦超级猛的整理,copy一份,快乐一下。
智能指针封装在了member中
auto_ptr
所有权进行传递,例如p2=p1
之后,p1实际上已经失效了
unique_ptr
所有权严格独占的模式,p2=p1
的语句会报错(除非p1是一个临时右值)
- 初始化方式和shared_ptr的初始化方式类似通过构造函数或者
make_unique
函数,参见shared_ptr相关信息 - 自定义deleter函数和shared_ptr不一致,使用了模板的方式进行实现
1 | class Base { |
shared_ptr
共享式拥有的概念,多个指针可以指向同一个资源,该资源会在最后一个指针被析构的时候释放
shared_ptr的所有成员函数都是线程安全的,如果多线程同时访问同一个
non-const
的shared_ptr,那有可能发生资源竞争(Data Race)的情况,比如改变这个shared_ptr的指向,这种情况下需要多线程同步机制。其内部包含两个指针,一个指向共享对象,另一指向共享计数区域。
通过构造函数进行初始化
若在构造shared_ptr的时候传入的是空指针,那么引用计数会是0
在构造时只能传入指向heap的指针,而不能是一个栈的地址
不允许通过一直原始指针初始化多个shared_ptr,会引起多次delete操作
允许自定义删除行为
1
2
3
4
5
6int *p = new int[10];
auto func = [](int *p) {
delete[] p;
cout << "Delete memory at " << p << endl;
};
shared_ptr<int> ptr(p, func);用途:删除数组,若指针指向的是一个数组,需要自定义删除(
deleter
)数组的行为(也可以采用C++17之后的标准支持)1
2
3
4
5
6
7
8
9
10
11
12class Basic {
public:
Basic() { cout << "Basic" << endl; }
~Basic() { cout << "~Basic" << endl; }
};
int main() {
Basic *p = new Basic[3];
shared_ptr<Basic> ptr(p, [](Basic *p){ delete[] p; }); // 自定义删除行为
// 或者
Basic *p2 = new Basic[3];
shared_ptr<Base[]> ptr(p2);
}
通过
make_shared
初始化接收的参数可以是一个对象,也可以是一个跟该类的构造函数匹配的参数列表
1
2auto ptr1 = make_shared<vector<int>>(10, -1);
auto ptr2 = make_shared<vector<int>>(vector<int>(10, -1));与通过构造函数初始化不同,
make_shared
允许传入一个临时对象,会将该对象拷贝到heap中和原来的对象的存储空间分离开;此外通过指针修改内容不会影响到原来的对象1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using namespace std;
int main() {
vector<int> v = {1, 2, 3};
auto ptr = make_shared<vector<int>>(v);
// &v = 0x7fffffffd9c0
// ptr.get() = 0x55555556dee0
cout << &v << endl;
cout << ptr.get() << endl;
// v[0] is still 1
(*ptr)[0] = 100;
cout << v[0] << endl;
return 0;
}
指向一个函数
1
2
3
4
5void func() { cout << "hello" << endl; }
int main() {
shared_ptr<void()> ptr(func, [](void (*)()) {}); // 必须自定义deleter的行为
(*ptr)();
}函数名称 含义 make_shared 传入构造函数 operator = 赋值函数 use_count 返回引用计数的个数 unique 返回是否是独占所有权( use_count 为 1) swap 交换两个 shared_ptr 对象(即交换所拥有的对象) reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少 get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptr<int> sp(new int(1));
sp
与sp.get()
是等价的
当两个shared_ptr相互指向的时候会出现循环计数的问题,从而造成内存泄漏,可以采用weak_ptr
协助解决
weak_ptr
不控制对象寿命周期的智能指针
weak_ptr
指针通常不单独使用(因为没有实际用处),只能和shared_ptr
类型指针搭配使用。不会改变引用计数的数量
weak_ptr
没有重载*
和->
运算符,因此weak_ptr
只能访问所指的堆内存,而无法修改它成员函数如下:
函数 作用 operator = weak_ptr
可以直接被weak_ptr
或者shared_ptr
类型指针赋值swap 与另外一个 weak_ptr
交换 own objetcreset 置为 nullptr
use_count 查看与 weak_ptr
指向相同对象的shared_ptr
的数量expired 判断当前 weak_ptr
是否失效(指针为空,或者指向的堆内存已经被释放)lock 如果 weak_ptr
失效,则该函数会返回一个空的shared_ptr
指针;反之,该函数返回一个和当前weak_ptr
指向相同的shared_ptr
指针。demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using namespace std;
// global weak ptr
weak_ptr<int> gw;
void observe() {
cout << "use count = " << gw.use_count() << ": ";
if (auto spt = gw.lock()) cout << *spt << "\n";
else cout << "gw is expired\n";
}
int main() {
{
auto sp = make_shared<int>(233);
gw = sp;
observe(); // use count = 1: 233
}
observe(); // use count = 0: gw is expired
}weak_ptr解决循环引用造成的内存泄漏的问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29class Parent;
class Child;
class Parent {
public:
shared_ptr<Child> childptr; // 换成weak_ptr<Child> childptr; 之后会出现析构才能正常
Parent() { cout << "Parent" << endl; }
~Parent() { cout << "~Parent" << endl; }
};
class Child {
public:
shared_ptr<Parent> parentptr; // 换成weak_ptr<Child> childptr; 之后会出现析构才能正常
Child() { cout << "Child" << endl; }
~Child() { cout << "~Child" << endl; }
};
int main() {
shared_ptr<Parent> parent(new Parent());
shared_ptr<Child> child(new Child());
parent->childptr = child;
child->parentptr = parent;
} // 没有产生析构的输出;
// 两处其实只需要一处修改成weak_ptr,打破环路即可
// parent child
// | |
// V V
// +---Parent---+ +---Child---+
// | childptr |---------->| |
// | |<----------| parentptr |
// +------------+ +-----------+
// 在堆上的指针不要是shared_ptr,因为存在于堆上,没有显示的delete操作会造成相互引用,谁都不能计数为0
C++ 习题
牛客网C++习题合集
C++和C之间的区别
在设计思想上:
C++是面向对象语言,而C是面向过程化的编程语言
在语法上:
- C++具有封装、继承、多态;
- C++相比于C,增加了与多类型安全的功能,比如强制类型转换;
- C++支持范式编程,比如模板类、函数模板等
指针和引用间的区别
原题链接 25,回答如下:
- 我理解的引用是一个const指针,其实也有自己的空间,对应如下内容:
- 指针有自己的一块空间,而引用只是一个别名();
- 指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象的引用(类比const类型的变量必须被初始化);
- 可以有const指针,但是没有const引用(本身就是const的了);
- 指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被改变;
- 指针可以有多级指针(**p),而引用至于一级;
- 指针和引用使用++运算符的意义不一样; ?
- 使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;
- 作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象;
- 如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。?
指针和数组间的区别
原题链接 29,我的理解是数组是一个指向连续存储空间的const指针,但是和指针之间有一些区别
- 对指针的加法操作
p+n
和数组arr+n
以及arr[n]
是相同的 - sizeof,对指针求大小是,返回的是指针本身的大小,而对数组求大小的时候,返回的是数组的整个大小
- 指针指向的内存地址通常是通过Malloc分配内存,free释放内存;而数组是隐式的分配和删除
- 数组名被解释为指向数组中第一个元素的地址;但是对数组名取址表示的是指向整个数组的地址。因此
arr+1
和&arr+1
有着本质上的区别
四种智能指针
原题链接 27,感谢建邦整理的资料支持。C++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11支持,并且第一个已经被11弃用。智能指针的基本原理是:智能指针是一个类,超过了其作用域范围,就会被自动调用析构函数,而析构函数就是一个可以用于自动释放资源的挂载点。
四种cast转换
原题链接 24 C++中四种类型转换是:static_cast, dynamic_cast, const_cast, reinterpret_cast
- const_cast:将const变量转换成非const变量
- static_cast:用于各种隐式转换:非const转const,void* 转指针等;用于多态向上转化,也可以向下转化,但结果未知
- dynamic_cast:动态类型转换(只能用于含有虚函数的类),用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。
- reinterpret_cast:几乎可以实现任意的转换
举例如下:
1 | class A{ |
为什么不使用C的强制转换?
答: C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。
函数指针
1 | char * fun(char * p) {…} // 函数fun |
C++源文件处理流程
- 预处理阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件
- 编译阶段:将经过预处理后的预编译文件转换成特定汇编代码,生成汇编文件
- 汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件
- 链接阶段:将多个目标文件及所需要的库连接成最终的可执行目标文件
静态链接和动态链接???
include查找顺序
原题链接 45,双引号和尖括号的区别主要表现在编译器预处理阶段查找头文件的路径不一样。
- 对于使用双引号包含的头文件,查找头文件路径的顺序为:
- 当前头文件目录
- 编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)
- 系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径
- 对于使用尖括号包含的头文件,查找头文件的路径顺序为:
- 编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)
- 系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径
内存分配问题
原题链接 46,查阅mmap和堆区之间的区别
new与malloc
原题链接 52,new和malloc之间的主要区别如下:
- new分配内存按照数据类型进行分配,malloc分配内存按照指定的大小分配;
- new返回的是指定对象的指针,而malloc返回的是void*,因此malloc的返回值一般都需要进行类型转化。
- new不仅分配一段内存,而且会调用构造函数,malloc不会。
- new分配的内存要用delete销毁,malloc要用free来销毁;delete销毁的时候会调用对象的析构函数,而free则不会。
- new是一个操作符可以重载,malloc是一个库函数。
- malloc分配的内存不够的时候,可以用realloc扩容。扩容的原理?new没用这样操作????。
- new如果分配失败了会抛出bad_malloc的异常,而malloc失败了会返回NULL。
- 申请数组时: new[]一次分配所有内存,多次调用构造函数,搭配使用delete[],delete[]多次调用析构函数,销毁数组中的每个对象。而malloc则只能sizeof(int) * n。
new/delete
会调用类的构造/析构函数,底层实际调用的是 malloc/free
,但 malloc/free
不会调用构造/析构函数。
new 申请的内存空间为
1
2
3
4
5
6| Bookkeeping | First Object | Second Object |....
^ ^
| |
| p2: This is what is returned by new[]
|
p1:this is what is returned by malloc()
当new baseType[n]
的时候,编译器会取消编译头
1 | auto ptr = new B[4]; // ptr指向的是上述的p2 |
内存泄漏
内存泄漏的分类
- 堆内存泄漏 (Heap leak),
malloc/new
和free/delete
相关的问题 - 系统资源泄漏:程序使用系统分配的资源,比如Bitmap,handle ,SOCKET等没有使用相应的函数释放掉
- 没有将基类的析构函数定义为虚函数。当基类指针指向子类对象时,如果基类的析构函数不是
virtual
,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。(影响范围?会影响到栈上的分配问题吗?)
内存泄漏的额检查办法:
- 使用内存泄漏检查工具
Valgrind
- 统计
malloc/new
和free/delete
的数量是否一致来判断,是否存在泄漏的情况
段错误
原题链接 50,发生段错误的两种情况:
- 使用野指针
- 试图修改字符串常量的内容
MMU在做逻辑地址到物理地址的转换时发生2次检查,在这两步均可能导致段错误
- 检查逻辑地址是否在某个已定义的内存映射区域,这一步通过和
mm_struct
中,mmap
指针所记录的vm_area_struct
链表中的每个每个节点所限定的虚拟内存区域比较实现。vm_area_struct结构中的vm_start
和vm_end
成员记录该节点所定义的虚拟内存区域的起始/结束地址(逻辑地址)。如果要访问的地址不在任何一个区域中,则说明是一个非法的地址。Linux在搜索vm_area_struct
时不是使用链表,而是使用树结构加速查找速度。
- MMU得到该地址的页表项,检查页表项中的权限信息,如果操作(读/写)与权限不符,则触发保护异常
共享内存相关的api
有时间系统了解吧
reactor模型
搭配IO复用系统了解一下
C++ 11 的新特性
https://www.nowcoder.com/ta/review-c/review?query=&asc=true&order=&page=56
编写一个strcpy函数
原题链接 4 ,网站上给出的答案感觉不太合理。
1 | char* strcpy(char *strDest, const char *strSrc){ // 使用const声明不破坏source的内容 |
编写一个String类
String类定义
1 | class String{ |
String类的实现,当类中包括指针类成员变量时,一定要重载其拷贝构造函数、赋值函数和析构函数,这既是对C++程序员的基本要求,也是《Effective C++》中特别强调的条款。注意剑指Offer中提到的安全问题。
1 | //普通构造函数 |
传入参数
下列代码有几个问题:(1)传参、(2)没有free操作、(3)free之后需要赋0,防止野指针、(4)printf
容易造成格式化攻击
1 | void GetMemory( char *p ) { // 拷贝赋值,不会影响调用者的值 |
正确的传参方式,以及编程习惯;
1 | // 传值调用 |
关于传递数组参数
数组参数char p[100]
,会失去本身的内涵,退化成一个指针,此外该指针还是去了const的属性,可以自增或者自减
1 | // 题一 |
acWelcome会被转化成一个指向字符串常量的指针,*acWelcome
为第一个字符
宏
宏定义的使用,宏定义可以类似实现函数的功能,但它不是真正的函数,只进行了参数的替换,因此需要严格的使用括号控制宏的范围
1 |
防止宏的副作用,例如使用上述MIN的方式如果如下
1 | MIN(*p++, b); // 将转换成下式 |
头文件中宏
1 |
|
C++支持重载,所以针对于void foo(int x, int y);
会编译成名字为_foo_int_int
,为了实现和C的兼容,可以在函数声明前面添加extern "C"
按照C语言的风格将上述函数转换成_foo
,来实现兼容的目的。
1 |
|
static和const的作用
static
的作用如下:
区分自动变量还是静态持续无链接性变量
- 函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;
区分变量或者函数的作用范围
- 在模块内的static全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问;
- 在模块内的static函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内;
在类中修饰变量和函数
在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝; (和static修饰函数体内的变量的本质一样jt)
在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量;(针对第4点提出的)
const
的作用
欲阻止一个变量被改变,可以使用const关键字
在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;
被const 修饰的变量,只能调用其自身的const函数,不能调用非const函数(也正是通过该特点,实现const对函数的重载)
对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;
在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量;
对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为左值。
例如:
1
2
3
4
5const classA operator*(const classA& a1, const classA& a2);
// operator* 的返回结果必须是一个const对象。如果不是,这样的变态代码也不会编译出错
classA a, b, c;
(a * b) = c; // 对a*b的结果赋值
// 操作(a * b) = c显然不符合编程者的初衷,也没有任何意义。
编写一个const返回值函数
顺着上个习题的const修饰类成员函数的返回值,在这里举一个实现上的小例子 TODO
union
union存放顺序的所有成员都是低地址到高地址。
- 小端存储,数据贴到了小地址位置,读数据时从大地址向小地址读
- 大端存储,数据贴到了大地址位置,读数据时从小地址向大地址读
类的大小(32位机器)
1 | class CTest |
若按照4字节对齐,sizeof(CTest)
= 12
若按照1字节对齐,sizeof(CTest)
= 9
c++继承
1 | class A |
野指针
请问如下代码的输出结果?
1 |
|
函数char *myString()中没有使用new或者malloc分配内存,所有buffer数组的内存区域在栈区
随着char *myString()的结束,栈区内存释放,字符数组也就不存在了,所以会产生野指针,输出结果未知
构造与析构顺序
1 | C c; |
这道题主要考察的知识点是 :全局变量,静态局部变量,局部变量空间的堆分配和栈分配。
其中全局变量和静态局部变量时从 静态存储区中划分的空间,
二者的区别在于作用域的不同,全局变量作用域大于静态局部变量(只用于声明它的函数中),
而之所以是先释放 D 在释放 C的原因是, 程序中首先调用的是 C的构造函数,然后调用的是 D 的构造函数,析构函数的调用与构造函数的调用顺序刚好相反。
局部变量A 是通过 new 从系统的堆空间中分配的,程序运行结束之后,系统是不会自动回收分配给它的空间的,需要程序员手动调用 delete 来释放。
局部变量 B 对象的空间来自于系统的栈空间,在该方法执行结束就会由系统自动通过调用析构方法将其空间释放。
之所以是 先 A 后 B 是因为,B 是在函数执行到 结尾 "}" 的时候才调用析构函数, 而语句 delete a ; 位于函数结尾 "}" 之前。
指针数组与数组指针
1 | int main() |
常量字符串
剑指Offer中提到的
1 | char *a = "Hello World!"; |
struct 和 class
默认访问权限
- 成员变量/函数的默认权限:struct 是 public 的,class 是 private
- 默认的继承访问权限:struct 是 public 的,class 是 private 的
sizeof的计算,有差别吗?其实不是很清楚
模板只能使用class,不能使用struct.
1
2
3
4template<struct T> // error!
struct S{
T *a;
};
struct的存在更多是为了适应C程序员的习惯
指针和引用
编译得到的机器码是类似的,都是获取变量的地址方式实现。引用只是C++对指针操作的一个“语法糖”,在底层实现时C++编译器实现这两种操作的方法完全相同。
指针 | 引用 |
---|---|
可以不初始化,可以为空 | 必须初始化,不能为空 |
可以更换指向的目标 | 不能更换指向的目标 |
重载、重写与重定义
- 重载 (Overload) :函数名相同,参数列表不同。
- 重写 (Override) : 类
A
定义了一个虚函数func
,其子类B
把函数func
重新实现(aka, 覆盖)。 - 重定义 (Refine) : 类
A
定义了一个普通函数func
,其子类B
把函数func
重新实现。
那么 Override 和 Refine 的区别在哪?答案是动态绑定。
类模板与模板类
类模板:声明带模板的类 模板类:具体化的类
模板类是在编译时期产生的,会生成具体的类
Leetcode C++突击
面向对象三大特征
封装、继承、多态
封装
把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。关键字:public, protected, private。不写默认为 private。
public
成员:可以被任意实体访问protected
成员:只允许被子类及本类的成员函数访问private
成员:只允许被本类的成员函数、友元类或友元函数访问
继承
- 基类(父类)——> 派生类(子类)
多态
- 多态,即多种状态(形态)。简单来说,我们可以将多态定义为消息以多种形式显示的能力。
- 多态是以封装和继承为基础的。
- C++ 多态分类及实现:
- 重载多态(Ad-hoc Polymorphism,编译期):函数重载、运算符重载
- 子类型多态(Subtype Polymorphism,运行期):虚函数
- 参数多态性(Parametric Polymorphism,编译期):类模板、函数模板
- 强制多态(Coercion Polymorphism,编译期/运行期):基本类型转换、自定义类型转换
强制类型转换运算符
static_cast
- 用于非多态类型的转换
- 不执行运行时类型检查(转换安全性不如 dynamic_cast)
- 通常用于转换数值数据类型(如 float -> int)
- 可以在整个类层次结构中移动指针,子类转化为父类安全(向上转换),父类转化为子类不安全(因为子类可能有不在父类的字段或方法)
向上转换是一种隐式转换。
dynamic_cast
- 用于多态类型的转换
- 执行行运行时类型检查
- 只适用于指针或引用
- 对不明确的指针的转换将失败(返回 nullptr),但不引发异常
- 可以在整个类层次结构中移动指针,包括向上转换、向下转换
const_cast
- 用于删除 const、volatile 和 __unaligned 特性(如将 const int 类型转换为 int 类型 )
reinterpret_cast
- 用于位的简单重新解释
- 滥用 reinterpret_cast 运算符可能很容易带来风险。 除非所需转换本身是低级别的,否则应使用其他强制转换运算符之一。
- 允许将任何指针转换为任何其他指针类型(如
char*
到int*
或One_class*
到Unrelated_class*
之类的转换,但其本身并不安全) - 也允许将任何整数类型转换为任何指针类型以及反向转换。
- reinterpret_cast 运算符不能丢掉 const、volatile 或 __unaligned 特性。
- reinterpret_cast 的一个实际用途是在哈希函数中,即,通过让两个不同的值几乎不以相同的索引结尾的方式将值映射到索引。
bad_cast
- 由于强制转换为引用类型失败,dynamic_cast 运算符引发 bad_cast 异常。
bad_cast 使用
1 | try { |
运行时类型信息 (RTTI)
dynamic_cast
- 用于多态类型的转换
typeid
- typeid 运算符允许在运行时确定对象的类型
- type_id 返回一个 type_info 对象的引用
- 如果想通过基类的指针获得派生类的数据类型,基类必须带有虚函数
- 只能获取对象的实际类型
type_info
- type_info 类描述编译器在程序中生成的类型信息。 此类的对象可以有效存储指向类型的名称的指针。 type_info 类还可存储适合比较两个类型是否相等或比较其排列顺序的编码值。 类型的编码规则和排列顺序是未指定的,并且可能因程序而异。
- 头文件:
typeinfo
typeid、type_info 使用
1 |
|
学习资源
- cppinsights 可以通过完善代码的方式,展现编译器为我们做的工作
- 牛客网 C++校招面试题合集
- 《C++ Primer Plus》
- 《STL 源码解析》
- 《深度探索C++对象模型》
待吸收
C++基础知识
- C++默认提供的函数
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值函数(operator=)
- 移动构造函数
- C++默认提供的全局操作符
- ,
- &
- &&
- ->
- ->*
- new
- delete
C++语法知识
DB() = default;
在C++11中使用了更精确的版本控制,该语句表示DB()
函数使用默认的构造函数。DB(const DB&) = delete;
与上句类似,不生成默认的拷贝构造函数。在之前的C++版本中会将函数写到private中,从而限制调用。#ifndef A_H
表示if not define a.h(如果不存在a.h文件)#define A_H
紧接上一条语句,实现头文件被重复include的情况发生explicit
修饰函数,阻止隐式类型转换extern
C/C++描述作用范围的关键字,修饰变量或者函数,被修饰者可以在本模块或其他模块使用static
和extern
相反,被它修饰的全局变量和函数只能在本模块中使用使用
extern "C"
在C和C++之间相互调用,参考volatile
用它声明的类型变量表示可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。所以使用 volatile 告诉编译器不应对这样的对象进行优化#pragma pack(n)
自定义以n字节方式对齐