基础篇
C 和 C++ 有什么区别
使用中比较直观的区别:
- c++ 使用 new/delete 运算符 取代 c 中的 malloc/free 库函数。
- new 的作用一是分配内存,二是调用类的构造函数;delete 同样除了释放内存外还负责调用类的析构函数。而 malloc/free 则只是分配和释放内存。于是无法使用 malloc 实例化一个对象。
- malloc/free 和 new/delete 都可以申请动态内存和释放内存,但是 new 可以自动分配空间大小,malloc 则需要使用 sizeof() 手动指明申请空间长度,比如申请一个长度为 length 的整型数组,new 方法只需要
int *p = new int[10];
,而 malloc 则需要手动指明空间大小如int *p = (int*) malloc(sizeof(int) * length);
- new 是类型安全的,因为其有类型检查功能,而 malloc 则不然,因为 malloc 只负责开辟空间,其他的一切要程序员自己负责。比如
int *p = new float;
无法通过编译,但是int *p = (int*) malloc(sizeof(float));
则可以正常编译。 - malloc 并不是被淘汰的概念,在需要申请无格式的的空间的时候其仍然有用。比如我们的协程服务器项目,协程栈的申请只需要指明栈大小,具体栈是什么格式存储什么类型数据,这个让协程中的函数自行去决定,就可以用 malloc 且也只有 malloc 方便。
- 使用 malloc 分配的内存后,如果在使用过程中发现内存不足,可以使用 realloc 函数进行内存重新分配实现内存的扩充,new 没有这样直观的配套设施来扩充内存。
- c++ 引入了 ”引用“ 的概念 。
- c++ 引入了 class 类 的概念,进而允许通过函数继承和重写实现多态。
引用和指针
- 指针是实际的变量,有自己的一块空间;引用只是代码层面的某个元素的别名,没有独立的空间。
- 64位系统下使用 sizeof 查看一个指针的大小是一个 8 ( 这是由于指针要求可以存储所有地址空间,32位处理器的逻辑寻址位数是32,也即 4 个字节;64为则是 8 个字节 ),查看引用则是被引用对象的大小。
- 指针使用过程中可以指向其他同类型对象,引用只是一个别名,就是对象本身,无法进行除对象自有以外的操作。
- 指针可以多级使用,引用止于一层。
struct 和 class 有什么区别
c++ 中 class 和 struct 的最本质的区别,class 是类类型,在堆中分配空间,栈中保存的只是引用,是地址;而 struct 是具体的值,在栈中分配空间。栈由操作系统管理,栈中分配的空间在生存期结束会自动释放;堆由程序员管理,堆空间需要手动释放。这也是 class 析构时需要手动 delete 分配的空间,否则会造成内存泄漏的原因。
其他表象区别:
class 可以定义析构器,struct 则不可以;
class 的构造器不需要初始化全部字段,struct 则必须初始化所有字段;
class 支持继承和多态,struct 不支持;
接上一条,所以 struct 成员不可以用 protected 关键字修饰;
class 适合大而复杂的数据结构,struct 适合于经常使用的一些数据组合成的新类型;
智能指针
智能指针是一个模板类,用以封装对象的裸指针以实现自动的内存管理。智能指针指向的对象计数为零时将自动释放 new 出来的内存,避免了内存泄露和空悬指针等问题。c++ 在 memory 头文件提供四种智能指针:
auto_ptr
,独占式拥有,同一时间只能有一个智能指针可以指向同一对象。由于其不能指向数组、不能作为 STL 容器成员等缺陷,c++ 11 不再建议使用该类型智能指针。如果程序中使用了该指针,也不会报错,但是会报 warning 。unique_ptr
,独占式拥有,同一时间只能有一个智能指针可以指向同一对象,该智能指针类型对象无法进行拷贝构造或者赋值构造,但是可以通过 move 方法(ptr1 = move(ptr2)
将 ptr2 对其封装对象的所有权转移给 ptr1,此后 ptr2 没有有效的指向)转移其对封装裸指针的所有权。shared_ptr
,共享式拥有,允许多个智能指针同时指向同一个对象。对象每多一个智能指针,指针计数就会加一;每减少一个智能指针,计数减一;当计数减少到零,智能指针调用析构,new 出来的资源被释放。
shared_ptr
常用的一些成员函数:release()
提前释放对资源的所有权,调用该函数后计数减一。use_count()
获取引用计数。unique()
返回是否独占资源,也即引用计数是否为一。swap()
交换两个智能指针所拥有的对象。get()
返回对象裸指针。
weak_ptr
,可以绑定到shared_ptr
但是不增加对象的引用计数,用来解决当同一对象的两个shared_ptr
相互引用时、引用计数永远不会降到 0 导致的死锁问题。
shared_ptr 与引用计数
- 构造函数中计数初始化为 1;
- 拷贝构造函数中计数值加 1;
- 赋值运算符中,左边的对象引用计数减 1,右边的对象引用计数加 1;
- 析构函数中引用计数减 1;
- 赋值运算符和析构函数中,如果减 1 后计数为 0,则 delete 释放对象。
- shared_ptr 的引用计数可以通过
.use_count()
方法获取。
特别需要注意,如果使用智能指针管理对象,应当仅使用智能指针。 考虑这样一种情况,首先 new 出来一个对象的裸指针,然后两个智能指针分别去包装这个裸指针。此时,两个智能指针的计数都是 1 。这种情况下一旦函数退出或者程序结束,两个智能指针都会尝试调用析构对象的析构函数并 delete 掉这个对象,就会发生对同一个对象 delete 两次的问题,程序就会 core dump 退出。
另一种情况,我们手动 new 出一个对象裸指针,然后用一个智能指针去封装它,随后又手动 delete 掉这个对象。此时如果 use_count()
打印智能指针的引用计数,结果仍然是 1 ,虽然其所封装的对象已经消失;而且,仍然会发生两次 delete 的问题。
所以,如果使用智能指针包装对象,应该一开始就 shared_ptr<T> ptr(new Class());
, 而不要先 new 出一个裸指针然后在封装,后者很容易因为自己的不谨慎而发生内存管理错误,尤其在错综复杂的大型项目中。
static 关键字
- 修饰局部变量时,该变量将在静态存储区分配内存,变量生命周期与程序而非所在的函数相同,当程序结束操作系统回收空间时,该变量生命周期结束。但是变量的作用域仍然是函数局部。其意义在于,静态存储区的变量只在函数首次调用的时候初始化,后续调用不再进行初始化。也就是说static 的局部变量具有记忆性,本次调用完毕函数退出,下一次再进入函数时,变量仍保持着退出时的值。
- 修饰全局变量时,该变量在静态存储区分配内存,变量生命周期依然与程序运行周期相同。全局变量加不加 static 的区别在于,static 修饰的全局变量作用域被限制在当前文件,而不加 static 的非静态全局变量作用于则是整个程序。由于被 static 关键字修饰过的变量具有文件作用域,如果程序包含多个文件的话,即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。这样就防止了同名变量的误用。
- 修饰类的成员变量时:变量属于该类而非实例化后的对象。所有对象只维持一份拷贝,从而可以实现不同对象间的数据共享;不需要实例化即可以访问;不能在类内部初始化,一般在类外部初始化,且初始化的时候不加 static。
- 修饰类的成员函数时:函数属于该类而非实例化后的对象。该函数不接收 this 指针,只能访问类的静态成员,且不需要实例化即可以访问。
- static 修饰类的成员变量(局部)和成员函数(局部)的一个典型应用是 Meyer’s Singleton Meyer 单例:
class Singleton { public: static Singleton& get_instance() { static Singleton singleton; return singleton; } // 禁用拷贝构造、转移构造和赋值重载 Singleton(const Singleton& _object) = delete; Singleton(const Singleton&& _object) = delete; Singleton& operator=(const Singleton& _object) = delete; private: Singleton() { // 私有化构造函数 } }
#define 和 const 有什么区别
- 编译器的处理方式不同。
#define
宏在与处理阶段展开为实际代码;而const
常量在编译阶段赋值。 - 类型和安全检查不同。
#define
宏没有类型,不作任何检查,仅仅是将代码展开;而const
常量是有具体类型的。 - 存储方式不同。
#define
宏仅仅是文本替换,在预处理阶段即展开为实际的代码,不分配存储空间,或者说存储在程序的代码段;const
常量有具体的类型,会在栈中分配空间。 - 作用域不同。
#define
宏不受定义域限制,程序全局有效,而const
常量值在其定义域内有效。 - 功能范围不同。
#define
宏可以用于函数,也即宏函数;结合上一条,可以使用宏讲一个很复杂的函数调用缩减为较短语句以方便调用。
#define 和 typedef
#define
宏和 typedef 都用来将一种复杂的写法替换成另一种简洁或者可读性高的写法,以提高程序的易读性,其区别在于
- 执行时间不同:define 宏不是可编译的代码,只是简单的字符串替换,在预处理阶段会被展开为实际的内容;typedef 属于代码正文,在编译阶段执行,有类型检查功能。
- 作用域不同:typedef 有作用域限定,define 不受作用域约束,只要在 define 后面使用都是正确的。
- typedef 是代码正文,一定要加句尾分号;define 宏定义则一定不要分号。
- c++ 11中可以用
using xxx = yyy;
代替typedef yyy xxx;
,前者个人认为更易理解。
#ifdef, #ifndef , #undef, #if, #elseif 等的作用
多用于头文件,对所包装的语句进行限定。
比如 一个文件中写明了 #define __XXX_H__
,如果有多个文件多级 include 了这个头文件,那这个文件的 Include 就会产生冗余。此时就可以使用 #ifndef __XXX_H__ #define __XXX_H__ #endif
进行限定,只有当没有发现该头文件时,才引入之。
再比如 #define #undef
,多用于对宏定义的限定,程序只在某部分需要一个宏定义,为了防止在其他地方改宏定义生效,可以 #define xxx sentance; #undef
将宏定义限定在 define
和 undef
之间。
const 修饰类中成员函数的函数体
如果一个成员函数不会或者不应该修改类内成员变量(通过任何方式,包括调用非 const 成员函数),则应将其声明为 const ,在函数声明后面加上 const 关键字。如果声明为 const 的成员函数中出现了修改成员变量或者调用非 const 成员函数的语句,则会发生编译错误。如下程序:
class Stack{
public:
void push(int elem);
int pop();
int getCount() count;
private:
int m_num;
int m_data[100];
};
int Stack::getCount() const {
++ m_num; // 企图修改成员变量,编译报错
pop(); // 企图调用非 const 成员函数,编译报错
return m_num;
}
总结 const 修饰成员函数体的几点规则:
- const 修饰的成员函数不能访问非 const 成员函数,即使该非 const 成员函数没有修改成员变量。
- const 修饰的成员函数不能修改成员变量。
- 对于第二点,除非成员变量被 mutable 修饰。则其可以被 const 修饰的成员函数修改。
volatile 的作用
告知编译器,该关键字修饰的变量不要做优化。也即,不需要将其转移到 cpu 寄存器,每次使用的时候就访问其在内存中的位置即可。多用于多线程编程中多个线程操作一个变量的情况。
一个频繁使用的短小成员函数,应该用什么实现?优缺点如何?
使用 inline
内联函数,编译器会将内联函数中的代码替换到函数被调用的地方。
优点:
- 在内联函数被调用的地方进行代码展开,省去了函数调用的时间,从而提高了程序运行效率。
- 相比于宏函数,内联函数在代码展开时,编译器会进行语法检查或数据类型转换,更加安全。
缺点:
- 预处理展开后代码膨胀,产生更多开销。
- 如果内联函数内代码块的运行时间比函数调用时间长得多,那么效率的提升并没有那么大。
- 如果在编译完成后修改成员函数为内联,所有调用该函数的代码文件都需要重新编译。
- 特别的,内联声明只是建议,是否内联由编译器决定,实际并不可控。
头文件和库文件,静态链接和动态链接
头文件。稍微大型的面向对象项目中,代码通常被根据功能分成很多部分,而写到一个源文件里。通常,函数或类的声明和具体实现是分开的,头文件中包含函数的声明,简洁易读,库文件中包含函数的具体实现,可读性较差但实现高效。库文件通过头文件向外暴露接口,编译时链接器就可以根据头文件中的信息找到函数的具体实现并链接到程序的代码段中。
库文件。库文件是函数具体实现的封装,通常以二进制而不是源码形式保存函数的具体实现,通过对应的头文件暴露接口。从而实现对具体实现保密的目的。通常,库文件分为静态库和动态库两种,在 linux 下前者后缀 .a
,后者后缀 .so
。两者的区别在于载入的时刻不同,静态库在编译时就全部被装载进可执行程序中,动态库在编译时仅做引用,在运行时动态载入内存:
- 静态库在编译时就被编译进了可执行程序,生成可执行文件后删除库文件,程序仍可运行。
- 动态库被编译进可执行程序的只有函数接口,运行时在库里加载具体功能。如果删除库文件,将导致程序无法运行。
- 静态库由于是静态嵌入,程序运行速度较快,但是程序体积较大,不易更新和维护,升级的话需要重新编译。
- 动态库由于是运行时动态加载,因此程序运行速度较慢,体积较小,升级程序不需要重新编译。
- 静态库的链接只需要将静态库文件添加到 g++ 后面即可,比如:
g++ main.cpp build/libtest.a -o helloworld
- 动态库的链接需要指明头文件的搜索路径(目录一级)和库文件的搜索路径(文件一级):
g++ -I /user/local/workflow/include -L /usr/local/workflow/libs/Workflow.so *.cpp -o main
上面
-I
选项就是指定头文件的搜索路径,精确到目录一级;-L
项指明库文件路径,精确到具体文件一级。
变量的声明和定义,外部变量
变量定义用于为变量分配地址和存储空间,还可以为变量指定初始值。每个变量有且仅有一个定义。比如 int a;
是一个定义。
变量的声明用于向程序表明变量的类型和名字,每个变量可以在多处声明。比如 a = 6;
是一个声明。
特别的,位于外部变量如 extern int a;
,其实是一个声明而不是定义,该语句和 extren a;
等价,声明变量 a 是一个已经定义了的外部变量。外部变量的定义出现在函数的外部,如:
void func() {
......;
extren a; //声明 a 是一个外部变量
......;
}
int a; // 定义外部变量 a 是一个 int 型变量
全局变量和局部变量,进程的五个数据段
- 全局变量是整个程序都可以访问的变量,作用域是整个程序,生存期也是整个程序,程序结束时所占内存随程序被释放。
- 局部变量是只有某个模块(函数,对象)可以访问的变量,作用域在模块内部,生存期是函数或者对象生命周期,函数退出或者对象析构时内存被释放。
- 全局变量位于全局数据段,程序开始即被加载;局部变量位于堆栈段。
linux 中进程拥有五个数据段,地址为从低到高一般是:代码段,数据段,BSS 段,堆段,栈段。其中,数据段、BSS 段和堆段一般是地址连续的,代码段和栈段另外存储。
- 代码段:用来存放完成编译的可执行文件(而不是源文件)的操作指令。代码段需要放置在运行时被非法修改,所是以只读的。
- 数据段:用来存放可执行文件中已初始化的静态变量和全局变量 。
- BSS段:用来存放可执行文件中未初始化的静态变量和全局变量,全部初始化为0。变量在 BSS 段中只是占位符,不占据实际磁盘空间(可执行文件的);
- 堆段:由程序员手动操控,空间按需动态向高位地址增长,存放类对象即其他 new 出来的资源。
- 栈段:由操作系统管理,空间按需动态向地位地址增长,存放函数参数值、局部变量等等。
拷贝函数 strcpy, sprintf, memcpy 的区别
- 操作对象不同,strcpy 的两个操作对象都是字符串;sprintf 操作源对象可以是多种类型,目的对象是字符串;memcpy 的两个操作对象可以是任意可操作的内存地址,不论何种类型。
- 执行效率不同,memcpy 最高,strcpy 次之,sprintf 最低。
- 实现功能不同,strcpy 主要实现字符串变量间的拷贝,sprintf 主要实现其他数据类型格式到字符串的转化,memcpy 主要是内存块之间的拷贝。
指针常量和常量指针
指针常量,一个指针本身是常量,指针自身初始化后无法改变,但是可以改变指针指向的对象的值。
常量指针,常量的指针,其指向的值无法改变,但是指针自身可以改变其指向。
常量指针和指针常量这样区分:定义从右往左读,遇到 ptr 就是 “ptr is a”,遇到 *
就是 “pointer to “。于是:
int a = 5, b = 6;
const int* ptr1 = &a; // ptr1 is a pointer to int const,ptr1 是一个指向常整型的常量指针,等效于 const a = 5; int* ptr = &a。
int const* ptr2 = &a; // ptr2 is a pointer to const int,ptr2 是一个指向常整型的常量指针,等效于上面。
*ptr1 = b; // 错误,该指针是一个常量指针。
ptr1 = &b; // 可以。
int* const ptr3 = &a; // ptr3 is a const pointer to int,ptr3 是一个指向整型的常量指针,指针自身是常量,初始化后其指向哪个地址就不能变了
ptr3 = &b; // 不可行,指针自身是常量,指向的位置初始化后不能改变。
*ptr3 = b; // 可以
悬挂指针和野指针
- 悬挂指针:当指针所指向的对象被释放,但是该指针没有任何变化,以至于其仍指向已经被回收的内存地址,这种情况下该指针被称为悬挂指针。从而,delete 对象后,原指向它的指针应该使其指向 nullptr 或者其他有意义的地址。
- 野指针:未初始化的指针称为野指针。
- 从而应当在指针初始化之初或者对象被 delete 之后将指针指向 nullptr
面向对象篇
面向对象的三大特征
- 封装:将客观事物封装为抽象的类,类拥有成员函数和成员变量,并可以将自己的数据和方法暴露给可信的类或者对象,对不可信的类或对象则进行信息隐藏。
- 继承:派生类继承基类的 public 和 protected 方法,可以直接使用。
- 多态:派生类通过重写的方式,使相同接口方法实现不同功能,
重载、重写(覆盖)与重定义:
重载(overload) ,指函数名相同,但它的参数数量、顺序或类型不同,返回值也可能不同的函数。具有以下特征:
- 在同一个作用域(同一个类中);
- 函数名相同;
- 参数(数量、顺序或类型)不同;
- 返回值可以不同;
- 可以是类的成员函数也可以是普通函数;
- 可虚可非虚。
重写(override,也称覆盖) ,是指派生类重新定义基类的 虚函数 ,具有如下特征:
- 不在同一个作用域(分别位于基类和派生类);
- 函数名相同;
- 参数(数量、顺序或类型)相同;
- 返回值相同,否则报错(参数相同,基类声明为虚函数,必是重写);
- 只能是类的成员函数;
- 基类成员函数必须声明为 virtual,且不允许 static 静态函数重写。派生类不必要加 virtual 关键字;
- 重写函数的访问修饰符可以不同。如基类中 private 的成员函数,在派生类中可以被重写为 public 。
- 特别的,可以使用
override
关键字修饰派生类继承的基类虚函数,则该情况派生类一定要实现该虚函数,否则无法通过编译。class Son{ public: virtual void funFromBase() override; // 该虚函数一定要在派生类中实现,否则编译报错。 }
重定义(也称隐藏) , 注意和重写区分:
- 不在同一个作用域(分别位于基类和派生类);
- 函数名相同;
- 返回值可以不同;
- 参数不同(数量、类型、顺序任一),则不论是否虚函数,基类的同名函数将被隐藏。
- 参数相同,若为虚函数,则为重写,返回值一定要相同;若非虚函数,基类同名函数将被隐藏。
虚/纯虚函数,虚/纯虚析构,抽象类
基类中声明为 virtual 并且在基类的一个或多个派生类中被 重写 的 成员函数 ,称作虚函数。
class Base {
public:
virtual void fun(); // 虚函数,在基类中声明
};
class Son: Base {
public:
void fun(); // 重写
}
虚函数是实现多态性的主要手段之一。多态是指用同一个函数名定义不同的函数,这些函数具有不完全相同有比较相似的功能,这样就可以使用同样的接口访问具有不同功能的函数,实现“一个接口、多种方法”。具体地,在基类中定义一个虚函数,他的派生类继承并重写该虚函数。不同的派生类对象接收同一个信息,调用相同的函数名,但是执行各自重写的虚函数,这样就利用虚函数实现了多态。更广义的理解,重载和重定义应当也算多态,这些只是定义和概念的问题,不必细究。
许多情况下,基类中不能对虚函数给出有意义的实现。比如一个日志输出器基类,只有当其派生出向文件或者向标准输出的有具体目的地派生类的时候,才可以给出具体有意义的实现,否则无异于虚空输出。但 这不是说纯虚函数不可以定义,实际上纯虚函数是可以有函数体的,比如空函数体或者打印一行提示等等,只是函数体必须定义在类的外部。基类中的虚函数只是提供一个接口标准,其面向具体场景的具体实现只能由该基类的派生类去完成。此时应当将之声明为纯虚函数。
class Base {
public:
virtual void fun() = 0;
};
当基类中出现纯虚函数,该基类被称为抽象类。抽象类无法实例化。 更 general 地,所有没有给出纯虚函数具体定义的类都无法实例化,也即,只有当抽象类的派生类重写纯虚函数后,该派生类才可以实例化。
使用虚函数的一个好处是,指向派生类的基类指针可以调用派生类重写的虚函数;但如果派生类中只是重定义了基类的同名函数,则实际调用的还是基类的函数。
class Base {
virtual void fun1() {printf("virtual Base::fun1()\n");}
void fun2() {printf("Base::fun2()\n");}
};
class Son: Base{
void fun1() {printf("virtual Son::fun1()\n");}
void fun2() {printf("Son::fun2()\n");}
}
int main(){
Base *p = new Son();
p -> fun1(); // virtual Son::fun1()
p -> fun2(); // Base::fun2()
return 0;
}
基类指针执行派生类对象,虽然方便,但存在一个问题,那就是基类指针在析构时不会调用派生类中的析构函数,如果派生类有堆区属性,析构函数为调用则堆无法释放,会造成内存泄漏。可以声明基类的析构函数为虚析构,这样 delete 基类指针;
的时候就会调用派生类的析构函数,释放派生类对象的资源。
析构函数也可以声明为纯虚函数,但是,和普通的纯虚成员函数不同的是,纯虚析构函数必须给出定义(纯虚成员函数则非必须),否则编译报错。析构函数是纯虚,该类也属于抽象类,无法实例化对象。
clas Base {
public:
virtual ~Base() = 0;
};
Base::~Base() {printf("Base destractor\n");}
静态多态与动态多态
多态是面向对象程序设计的一个重要特征,其字面理解为:一个接口,多种实现。依照这种字面理解,多态可以分为两种:编译时多态和运行时多态。
- 编译时多态是静态多态,因为它在编译时就可以确定调用的接口,可以通过函数重载、重定义和泛型编程实现。
- 运行时多态是动态多态,因为它只有到具体运行到函数调用语句时才可以确定接口,只能通过继承和虚函数重写实现。运行时多态的实现机制称为动态绑定
静态多态和动态多态的区别在于什么时候将函数实现和函数调用关联起来,是编译时期还是运行时。
静态多态是指在编译期间就可以确定函数的调用地址并生产代码,通常通过函数重载和泛型编程。不同的同名函数往往只有函数名相同,其参数列表或返回类型是不同的,在编译期间编译器即可以通过函数表确定具体调用哪一个实现。于是编译时多态也被叫做静态绑定。
动态多态有三个要素缺一不可:继承、重写、基类指针指向派生类对象。由于继承重写的虚函数函数名、返回值和参数列表完全相同,编译时无法确定具体调用哪一个实现。比如一个网络连接接口,存在为手机端和PC端两个派生类,在新的客户端连接到来之前,程序无法确定具体调用哪一个实现。这种运行时多态也被叫做动态绑定。
模板函数和模板类的特例化
模板特例化是对模板中的某个特定类型给出特殊的实现。
一个模板函数或者模板类是对于所有类型都进行相同操作的统一接口集合,可以适应多种类型的需求。但如果对于其中某种类型,需要实现特有的功能,就要进行模板特例化。
模板函数的特例化给出一个空的 template<> 列表,表示这是一个模板函数的特例化,后面接函数声明,函数声明中给出具体的而非模板的参数类型,如:
template<class T> // 模板函数
bool compare(const T& v1, const T& v2) {
return v1 > v2 ? true : false;
}
template<> // 模板函数特例化,对C 风格字符串的比较需要给出另外的实现
bool compare(const char* &v1, const char* &v2) {
return strcmp(v1, v2);
}
模板类的特例化给出一个空的 template<> 列表,表示这是一个模板类的特例化,后面接指明具体类型的模板类,给出特别的类的实现,如:
template<class T> // 模板类
class Class {
... ...;
};
template<> // 模板类的特例化
class Class<const char*> {
... ... ; // 给出特殊实现
};
特别的,模板类可以对类的特定成员函数进行特例化,实现形式与类的特例化相同:
template<class T> // 模板类
class Class {
void fun1();
T fun2(T& v);
};
template<> // 特例化的声明
void Class<int>::fun1() { // int 类型 fun1 的特例化
// 给出具体实现
}
template<> // 特例化声明
const char*& fun2(const char*& v) { // const char*& 类型的特例化
// 给出具体实现
}
动态绑定怎么实现的
动态绑定指在程序运行时才绑定接口具体实现的行为,是多态实现的具体形式。
c++ 中的动态绑定通过虚函数实现,虚函数入口偏移通过虚函数表(v-table)记录,通过虚指针 v-ptr 访问。存在虚函数的类同时会维护一张虚函数表,记录各个虚函数入口地址(的偏移量);当有虚函数的类被实例化为对象,编译器会为之创建一个虚指针指向虚函数表的头部虚函数的入口地址。
派生类会完全继承基类的虚函数表,这句话这样理解:
- 对于派生类没有重写的基类虚函数,其函数入口在虚函数表中的位置保持不变(基类中是第 n 个,派生类中还是第 n 个);
- 对于派生类中首次出现,而基类中没有的虚函数,其排在原有虚函数后面,不会占有原有虚函数位置;
- 对于派生类中重写的虚函数,派生类重写的虚函数将替换基类虚函数的位置,比如基类虚函数表中第 n 个被重写,则派生类中重写的这个虚函数也在虚函数表第 n 个。
这样一来,编译器实现代码中函数调用语句时只需要确定调用了第几个个虚函数,进而确定虚指针的偏移即可,就可以确定虚函数的地址。由于不同派生类中同一个虚函数在虚函数表中的地址是一样的,则实际调用哪个实现可依具体情况(基类指针指向了哪一个派生类对象)而定。
虚表和虚表指针的深入理解
- 类的 每个对象有自己独立的虚拟内存空间。
- 虚表属于类的,类的所有对象共享同一个虚函数表。虚表位于类最开始的只读数据段,记录了每个虚函数的内存地址。
- 虚函数表是编译时期就生成的,而不是在构造函数中。
- 每个对象拥有一个虚指针 v-ptr,虚指针位于对象的最开始位置,是一个双层指针,理解为一个指针变量的数组。
- 于是取对象的地址,取到的就是虚表指针的地址。
- 虚指针指向虚函数表,虚函数表记录着虚函数地址,同类对象共享一个虚表,虚函数地址在对象的独立的 虚拟内存空间 是相同。由于 1 ,是每个对象自己的虚拟空间,虽然地址相同,但是是对象自己的虚函数。
- 虚指针在 x64 下面长度是 8 字节的 long long 。
为什么构造函数不能定义为虚函数 / 为什么基类的析构函数需要定义为虚函数
构造函数:虚函数的调用依赖于虚函数表,指向虚函数表的指针 vptr 在构造函数中被构造。将构造函数定义为虚就发生循环依赖了。
析构函数:为实现动态绑定需要使用基类指针指向派生类对象,当基类析构不是虚函数,在对象销毁时只会调用基类的析构,于是如果派生类申请了堆资源就无法释放,产生内存泄漏。只有当析构为虚,基类指针指向的派生类对象销毁时会调用派生类析构函数释放派生类对象资源。
多继承存在什么问题,如何消除多继承中的二义性
多继承也即一个派生类继承有多个基类。若基类之间发生成员同名,将出现对成员访问的不确定性,即 同名二义性。
当派生类从多个基类派生,而这些基类又拥有一个或多个共同基类,则在访问该共同基类成员时,又会产生 路径二义性。
可以利用域运算符 ::
限定派生类使用的是哪个积累的成员消除二义性。
没有任何成员的空类的大小是多少?为什么?
c++ 规定类的大小不能为 0 。一个没有任何成员的空类 A,对其取 sizeof(A) 的值是 1,也即一个字节。这是因为:
- 将类实例化为对象时,new 需要为不同实例分配不同的地址,其显然无法分配一个空间大小为 0 的地址。
- 避免除以 sizeof(A) 时发生除以 0 的错误。
空类有哪些成员函数
- 缺省构造函数
- 缺省拷贝构造函数
- 缺省析构函数
- 赋值运算符
拷贝构造和赋值运算符重载之间有什么区别
拷贝构造函数用于构造新对象:
Class s;
Class s1 = s; // 隐式调用拷贝构造函数
Class s2(s); // 显式调用拷贝构造函数
赋值运算符重载用于将源对象的内容拷贝到目标对象中,若源对象中包含未释放的内存需要先将其释放:
Class s;
Class s1;
s1 = s; // 使用重载的赋值运算符
C++ 提供的四种类型转换cast函数,为什么不使用 C 风格强制类型转换
C++ 提供了四种类型转换函数: const_cast
, static_cast
, dynamic_cast
, reinterpret_cast
。
const_cast<target type>(source variable)
:用于将 const 变量转换为非 const 变量。static_cast<target type>(source variable)
:可用于各种类型转换比如 double 到 int, 非const 到 const, void* 指针到有类型指针。dynamic_cast<target type>(source variable)
:动态类型转换,用于有虚函数的类类型的转换。即可以实现派生类到基类的向上转换,也允许基类到派生类的向下转换。只允许指针或引用,如果转换非法(无继承关系等情况),对于指针返回 nullptr,对于引用抛出bad_cast
异常,可以通过 try-catch 捕获。reinterpret_cast<target type>(source variable)
:任意类型转换,如 int 转指针类型,效果近似于 C 风格的强制类型转换。- 为什么不使用 C 风格强制类型转换:据说没有类型检查容易出错,但是自己没遇到过错误。编码中应严格避免过于离奇的类型转换。
实现数值转换的方法
基本类型之间可以使用强制类型转换或者C++ 风格 static_cast 类型转换函数,字符串转为基本类型也有 atoi
, stoi
, atof
, stof
。除此之外,boost 库提供了一种更方便的数值转换方法:lexical_cast
。这是一个模板函数,相比于强制类型转换和 atoi 等转换函数,lexical_cast
的优势在于:
- 模板函数,统一接口。
- 如果发生错误会抛出
bad_lexical_cast
异常,可以通过try-catch
语句捕获异常而不影响程序后续运行。
STL 篇
什么是 STL
STL 包括容器,算法和迭代器:
- 容器是数据的组织形式,包括序列式容器和关联式容器。
- 算法是作用于容器的,包括排序、复制等常用算法,以及不同容器自有的算法。
- 迭代器是在不暴露容器内部结构的情况下对容器的遍历。
什么时候用 unordered_map,什么时候用 map
unordered_map
是基于哈希表实现的,查找的时间复杂度是 O(1),而 map 则是红黑树实现的,查找的时间复杂度是 O(log N)。哈希实现除了搜索本身还有哈希函数的构造和查找耗时,另外哈希表的实现也会占用更多内存,于是:
- 如果数据量很小,使用
map
即可,由于map
和ubordered_map
的性能差异在数据量较小时并不明显。 - 如果数据量较大:
- 对内存空间要求更严格,使用
map
。 - 对速度要求更严格,使用
unordered_map
。
- 对内存空间要求更严格,使用
unordered_map 底层机制
unordered_map
基于哈希表实现,事实上,STL 中的所有无序容器(unordered_set
,还有什么?)都是哈希表实现的。哈希表的头指针使用 vector 存储,只有向后而没有向前的迭代器。通过 除留取余法 进行哈希(哈希函数),通过 开链法 实现 hash 冲突避免。由于是开链法,也即如果有哈希值相同的元素,就接在相同头结点最后一个节点后面,形成一个链表;从而哈希表的头指针 vector 每个元素都可以是一个链表,这个链表称为 bucket。
在 bucket 的数量设计上,哈希表内值了 28 个质数,在创建哈希表时,会根据存入的元素个数选择大于等于元素个数的质数作为哈希表的容量。每个 bucket 链表允许的最大元素数量也等于这个质数,当哈希表插入的元素数量大于了 bucket 允许的长度,就会以下一个质数为容量重建哈希表,并重新计算每个元素在新的哈希表中的位置。
vector 底层机制
vector 的内存空间:
vector 是一段连续的线性内存空间作为底层的数组,拥有三个迭代器,分别指向数组头、数组尾、和(包括未使用的)整块连续空间的尾部。
vector 的内存增长机制 :
当现有空间无法容纳新数据时,vector 以2倍(Linux gcc 的实现))或者1.5倍(windows visual studio 的实现)的大小自动申请一段更长的空间,将原来的数据拷贝到新的内存空间,并释放原空间。因此,对 vector 的任何操作一旦导致了其空间的重新配置,指向原 vector 的迭代器就失效了。
vector 的内存增长因子:
在 visual studio 下是1.5 倍,这样做的好处是,每次空间增长为 new = 1.5*old
,当连续增长到某一次,当前 size 的下一个需要的空间就小于此前释放掉的所有空间总和了,就可以可以前面释放掉的连续的空间而不用一直向后扩展。而如果增长因子是 2 每次需要扩增的空间永远大于此前释放掉的所有空间总和,就只能一直向后扩展,而不能复用前面已经释放掉的空间。
reserve 和 resize 的区别:
- reserve 是改变 capacity,也即预留空间大小,但是并不实际地添加元素,reserve 只有一个参数,就是 capacity 空间大小,如果数组原来位置没有元素,则 reserve 后仍没有。reserve 的作用是优化 push_back 和 insert,使其在加入新元素是不必考虑空间扩增问题了。
- resize 则是改变 size,也即实际有效空间的大小,resize 后新申请的空间里是有元素的。resize 有两个参数,第一个是改变后空间的大小,第二个是新加入容器的元素。
vector 的元素可以是引用吗:
不可以,vector 底层是一段连续的内存空间,引用不是实际存在的变量,自身没有大小,没有实际地址,因此 无法作为 vector 要求的空间连续的元素。
vector 迭代器失效与 earse:
当容器由于内存增长导致空间重新配置,迭代器全部失效。
当 erase 一个元素导致其后面的所有元素发生空间变化也会导致迭代器失效;erase 方法将返回下一个有效的迭代器,所以当删除某元素的时候应当 it = vec.erase(it)
。
vector 的内存释放:
vec.clear()
:清空内容,不释放内存。vector().swap(vec)
:使用一个空数组置换原数组,释放内存,得到一个新数组。vec.shrink_to_fit()
:请求降低 capacity 从而适配其 size。vec.clear()
和vec.shrink_to_fit()
一起使用也可以清空内存。
list 底层机制
list 底层是一个双向链表,以节点为单位存放数据,节点的地址在内存中不一定连续,每次插入或者删除一个元素,就额外配置或是放一个元素空间。list 由于不支持随机存取,而其插入删除的复杂度是 O(1),适合需要大量插入删除而不关心随机存取的场景。
deque 底层机制
deque 双端队列是双向开口的连续线性空间,在头尾两端执行插入和删除操作的时间复杂度都是 O(1),相比之下 vector 在头部插入和删除元素的时间复杂度是 O(n) 。另一点与 vector 不同的是,deque 不保证所有元素都存储在连续的内存空间中。当需要频繁在首尾两端插入和删除元素的时应首选 deque。
map, set, multimap, multiset 底层机制
multimap 指键值之间的对应关系是多对多而非一一对应的,multiset 相比 set 则是一个元素可以出现多次,count(key) 返回 key 出现的次数。
这四种容器的底层实现都是红黑树,从而其元素都是有序的,map/multimap 按照 key 值排序,其增删查改效率是 O(log N)。
为什么 map 和 set 的插入删除效率比序列容器高:
因为不涉及内存拷贝与内存移动,应该和链表类似。关联型容器以节点存储元素,每个元素拥有指向子节点、前一和后一结点的指针,插入和删除元素时不发生大量元素拷贝或移动,只是将某个节点指针指向新元素、或者将指向被删除元素的指针指向其他元素即可,因而效率较高。
插入或删除元素后,map/set 的迭代器会失效吗:
不会失效,因为没有内存变动。vector 之所以会失效,因为 insert 或 push_back 可能导致容器的内存超过其可用总容量,从而容器扩展到、元素拷贝到新的内存空间,指向原内存地址的指针自然就失效了。而 map/set 和 list 类似,元素本身未必连续,而是以节点方式通过指针相连接,插入或者删除元素不会导致元素所处的内存空间变化,自然迭代器也不会失效。
红黑树
红黑树是一种排序二叉树,用来解决排序二叉树在极端情况下退化链表导致查询效率由 O(log N) 退化为 O(N) 的问题。红黑树在每个节点上添加一个存储为表示节点的颜色,颜色只能是红或者黑。红黑树具有以下性质:
- 每个节点要么是红色,要么是黑色。
- 根节点永远是黑色。
- 所有叶子节点都是黑色,且一定是空节点。
- 每个红色节点的两个子节点都是黑色 –> 每个叶子节点到根节点的路径上不可能有连续两个红色节点。
- 从任一节点到其子树的每个叶子节点的路径上都包含相同数量黑色节点。
妈的,真他妈难,不看了。
内存管理篇
new/delete 和 malloc/free 的区别
- new 可以自动判断类型,申请合适大小的空间;malloc 则是无类型的,申请多少字节的空间需要程序员指定。
- new 可以调用对象构造函数,delete 可以调用析构函数;malloc/free 只负责申请和释放内存。
- new 是类型安全的,在其申请空间的时候会做类型判断,比如
int *p = new float[2]
无法通过编译;malloc 则只负责申请空间没有类型判断功能,如int *p = (float*)malloc(2*sizeof(float));
可以通过编译。 - malloc/free 需要引入库文件 stdlib.h ,new/delete 不需要库文件。
delete/free 和空悬指针问题
调用 delete 或 free 释放掉一块内存空间之后,内存清空了,指针还是指向这块被清空的内存地址的,此时如果不将指针指向 nullptr 空地址,该指针就成为一个空悬指针。
delete 和 delete []
对于基本类型而言,不论是不是数组形式,两种方式都可以释放内存。
对于自定义类型,单个对象使用 delete,对象数组使用 delete [] 逐个调用析构函数释放其内存。如果反过来,单个对象调用 delete[], 对象数组调用 delete,行为是未定义的,调用直接崩溃。
所以最恰当的方式是一一对应,用 new 申请就用 delete 释放,用 new [] 申请就用 delete [] 释放。
malloc 或 new 无法申请到充足空间该怎么处理
当剩余内存块太小, malloc 或 new 无法成功申请空间,malloc 会返回空指针,于是在 malloc 可能会产生空指针的语句需要 手动判断其返回值,如果是空指针应当 return 出这个函数或者 exit 掉整个程序,避免发生意外。
new 则默认抛出 bad_alloc
异常,应当用 try...catch...
代码块捕获异常并处理:
try{
int* p = new int[10000];
} catch (bad_alloc &memExp) {
cerr << memExp.what() << endl;
return;
}
内存泄漏的场景有哪些
内存泄漏也即内存申请了但是没有释放、程序生命周期无法在使用这一片内存的问题。总结为 new/delete 或 malloc/free 没有成对使用。具体体现为包括但不限于以下几点:
- 成员函数或者普通函数通过 new/malloc 在堆中分配的内存没有调用 free/delete 显式释放。
- 在构造函数中申请的内存,析构函数中没有释放。
- 使用多态特性时,派生类申请了堆内存,但是基类析构函数没有定义为虚,则析构时不会调用派生类析构函数,派生类申请的资源无法释放,导致内存泄漏。
- 还有一种操作可能导致非内存泄露的内存错误:未定义拷贝构造函数或者未重载赋值运算符导致的浅拷贝内存两次释放。比如,类中包含指针成员变量,如果没有定义拷贝构造函数或者重载赋值运算符,编译器会调用默认的拷贝构造或者赋值运算符,以逐个成员拷贝的方式复制指针成员变量,使得两个对象包含指向同一片内存的指针。两个对象析构的时候释放同一片内存空间,也会产生内存泄露。
c++ 内存模型
C++ 中内存从低位到高位分别是:
- 只读区。存放代码和常量等。
- 可读写区(低位 .data,高位 .bss)。存放全局变量和静态变量,已初始化存放于 .data,未初始化的存放于 .bss。
- 堆区。由程序员手动申请释放。
- 栈区。存放参数值、局部变量等。
其中 只读区、可读写区、和堆区是连续的,堆区可以向上扩展,栈区向下扩展。
C++ 中的堆和栈
- 管理方式
栈区:编译器自动分配释放,存放函数的参数值和局部变量。
堆区:程序员主动分配释放,若申请了但没有释放,会发生内存泄漏,但程序结束后操作系统会回收这一片内存。 - 分配方式
栈区:连续的内存空间,分配类似于数据结构中的栈,后入先出,分配方式内置于CPU 指令集。
堆区:费连续的内存空间,类似于数据结构中的链表。 - 申请后系统的响应
栈区:只要栈内剩余空间大于所申请空间,系统为程序提供内存,否则抛出栈溢出异常。
堆区:操作系统维护一个记录空闲内存地址的链表,当系统受到申请后遍历这个链表,找到第一个空间大于申请空间的节点,将此节点从链表中删除,并将该片内存分配给程序。如果节点内存大于所申请内存,则将剩余内存重写添加到链表中。操作系统会在内存首地址记录分配的内存大小,以便 delete / free 时正确释放。 - 内存区及大小
栈区:连续内存,其大小是操作系统规定好的,不同发行版各有不同。linux 系统中一般是 8M 或者 10 M,ubuntu1804LTS_x64 发行版是 8M。通过ulimit -s
指令可以查询。
堆区,非连续内存,大小没有限制。操作系统维护一个空闲内存地址链表,在链表中选择第一个大于申请空间的节点分配给程序。 - 申请效率
栈区:连续空间,编译器直接从栈顶分配,速度快。
堆区:非连续空间,需要程序员手动 new,遍历链表寻找,速度慢。 - 存储内容
栈区:第一个进栈的是主函数中调用当前函数语句的下一条可执行语句地址。然后是函数参数、局部变量。静态变量不入栈。
堆区:头部一个字节存储堆的大小,其余具体内容由程序员安排。
静态分配内存(栈内存)和动态分配内存(堆内存)的区别
- 静态内存分配在编译期间完成,不占用 CPU 资源;动态内存分配发生在运行期,分配和释放占用CPU资源。
- 静态内存分配发生在栈上,动态内存分配在堆上。
- 静态内存分配不需要指针或引用类型的支持,动态内存分配需要。
- 静态内存分配在编译前已确定内存块大小,动态内存分配在运行时才能确定。
- 静态内存分配由编译器管理,动态内存分配由程序员管理。
- 静态内存分配运行效率比动态分配高,动态内存分配允许的容量比静态大。
静态建立对象和动态建立对象
A a;
这种方式是静态建立对象,内存在栈上分配;
A* a = new A();
是动态建立,内存在堆上分配。
如何构造一个类,使其只能在堆或栈上分配内存
只能在栈上分配内存:
静态建立对象,并且类内将 new 和 delete 方法重载为 private ,是类无法申请堆空间。
只能在堆上分配内存:
活人不会这么做,暂不讨论。
深拷贝与浅拷贝
深拷贝与浅拷贝问题发生在指针拷贝上,浅拷贝只拷贝指针地址,浅拷贝后两个指针指向同一片内存空间,也即共享同一片内存;深拷贝则是开辟一块新的空间,拷贝原指针指向内存空间内的元素到新空间。
结构体内的字节对齐
假定从偏移为零的位置开始存储成员,可以使用 #pragma pack(n)
指定对齐大小,如不指定,则按照结构体内最大成员对齐:
如果没有定义 #pragma pack(n)
:
- sizeof 的结果必然是结构内部最大成员的整数倍,不够则补齐;
- 结构体内各成员的首地址一定是其自身大小的整数倍。这句话理解为,如果有一个 char 类型(1字节),一个 int 类型(4字节),这个 int 类型的首地址必不可能是 5,如果其在第二位,一定是 8。由于按照结构体内最大成员大小,char 类型补齐为4位。
如果定义了 #pragma pack(n)
:
- sizeof 的结果必然是
min(n, 结构内最大成员)
的整数倍,不够则补齐; - 结构体内各成员的首地址一定是
min(n,结构体内最大成员)
的整数倍。
malloc 申请的内存是否可以 delete 释放,new 申请的内存是否可以 free 释放
实验显示,基本类型可以,对象不行。
由于 malloc 无法调用构造函数,也自然不存在 malloc 一个对象用 delete 释放的问题。
由于 free 不具有调用析构函数之功能,其无法正确释放 new 出来的对象内存,可能导致对象内部申请的内存不释放。
C++ 11 新特性
- auto 自动类型推导。
- 右值引用和 std::move() 函数。
右值引用的类型表达是类型后面跟上两个与:T&&
。左值是指表达式结束后依然存在的持久化对象,是有地址的;右值是指表达式结束时就不再存在的临时对象,是无法取地址的。左值一般在内存中,右值一般在内存或CPU寄存器中。int i = 10; // 持久存在的可以取地址的变量,是个左值。 int &l = i; // int& 类型后面一个 &,这是一个左值引用 // int &l = 10; // 10 是个临时值,是无法取地址的右值。左值引用无法绑定右值。 int && r = 17; // 17是个无法取地址的右值,通过引用可以绑定它。 // int && r = i; // 但是右值引用无法绑定左值 l = r; // 左值引用无法绑定右值,但可以绑定右值引用啊 l = 13; cout << r << endl; // 结果是 13
右值引用的用处之一是可以作为构造函数的参数,作为一种新的 移动构造 以减少内存消耗提高效率。
在没有右值引用之前,我们传入一个临时变量作为构造函数的参数比如A a(new A("ABC"));
实际上是调用的拷贝构造函数A(const A& a);
参数类型是一个常量左值引用,虽然这个临时变量new A("ABC")
的作用和生命周期仅限于实例化 a 这个对象,但这个临时变量还是要先构造出来,然后再传给 a 作为拷贝构造的参数。简单地说,临时构造出来的这个对象,除了作为参数完成拷贝构造外别无他用,这无疑是一种资源浪费。
C++ 11 引入了右值引用,允许我们定义一种新的构造函数:移动构造函数 ,相同的构造语法A a(new A("ABC"))
但是优先调用的却是移动构造,传入的参数是右值引用A(A&& a);
。在移动构造函数中,我们直接使用参数传入的右值引用申请的空间和具有的资源占为己有,然后将右值引用的指针指向 nullptr。减少了一次资源拷贝。
比如类拥有一个字符数组成员变量char* m_data;
,如果是拷贝构造,传入的类类型变量是一个常量左值引用对象str
,则需要m_data = new char[sizeof(str.m_data)]
先申请出空间,然后strcpy(m_data, str.m_data)
完成一次复制。构造结束后临时变量str
即被销毁。
如果是移动构造函数,则直接将临时变量的资源占为己有m_data = str.m_data;
、然后将临时变量指向该资源的指针指向 nullptrstr.m_data = nullptr;
即可,这样相比于 拷贝构造函数就减少了一次不必要的空间申请和资源复制。综上,以右值引用做为参数的移动构造既节省资源,又节省资源申请和释放的时间。那如果是一个已经被构造出来的左值对象,可以像临时变量那样调用移动构造函数吗?
std::move() 是包含在<utility>
头文件里的标准库函数,它将一个左值转换为右值,以告知编译器优先调用移动构造函数。 - 列表初始化。c++ 11 可以在变量名后面跟上一个花括号包装的列表给对象的成员变量赋值,进行对象初始化。
- std::function 函数包装器、std::bind 绑定函数、lambda 表达式。其中匿名 lambda 的语法是
[](){}
,三个括号中的内容分别是捕获列表、参数、函数体。function 可作为函数包装器可以包装包括 lambda 在内的一切类型的函数,function 的作用置一是可以作为函数参数接受一个函数,而避免使用复杂的指向函数函数地址的指针的方法。bind 的作用,可以和function 函数包装器配合使用向fucntion<void(void** argv)>
无格式参数函数里面传入参数。 - 提供了一些线程相关的标准库函数如 std::thread, std::mutex, std::lock, std::atomic。
- 提供了三个智能指针 std::shared_ptr, std::unique_ptr, std::weak_ptr 。
- 允许了基于范围的 for 循环如:
for (const &i: vec)
。 - 允许派生类继承基类构造函数。
- 提供了 nullptr 常量作为空指针,而不是 NULL 宏。NULL 宏实际上是个 int 类型值为 0 的变量;而 nullptr 是个 void* 指针类型。
- 提供了 final 和 override 两个关键字。final 用于修饰一个类,表示该类禁止进一步派生和虚函数重载;override 修饰一个虚函数,表示这个该函数一定是个继承自基类的虚函数,如果 override 修饰的该虚函数没有重写或者基类没有这个虚函数,编译报错。
- 提供了多用于构造函数的修饰符 default, delete, explicit,用法是
Constructor([argv]) = default/delete/explicit;
,含义分别时声明该构造函数是缺省的、是不可调用的、是只能显示调用的。其中 delete 修饰符常用于拷贝构造函数和赋值重载函数,以实现不可拷贝类的功能。 - 提供了有作用域的枚举类型
enum class
,从而不再允许不同作用域的枚举型变量的比较。 - sizeof 可以直接作用于类的非静态数据成员。
- 引入了 thread_local 线程局部变量修饰符,被 thread_local 修饰的变量每个线程仅维护一份。
C++ 中的流风格 IO
C++ Primer 中文版, 电子工业出版社, 第五版 309 页第八章序:
C++ 语言不直接处理输入输出,而是通过一组定义在标准库中的类型来处理 IO。这些类型支持从设备读取数据、向设备写入数据的 IO 操作,设备可以是文件,控制台窗口等等。还有一些类型允许内存 IO,即从 string 读取数据,向 string 写入数据。
如图所示,c++ 标准库提供三个 IO 流相关头文件:iostream
,, fstream
, stringstream
分别用于通用输入输出、文件输入输出和字符串输入输出。流是一种抽象概念,它代表了数据的无结构化传递。按照流的方式进行输入输出,数据被当成无结构的字节序或字符序列。从流中取得数据的操作称为提取操作,而向流中添加数据的操作称为插入操作。用来进行输入输出操作的流就称为IO流。换句话说,IO流就是以流的方式进行输入输出。