一、 C++
1、static关键字的作用
2、C++虚构函数为什么要为虚构函数
基类指针可以指向派生类的对象(多态性),如果删除该指针delete []p;就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。所以,将析构函数声明为虚函数是十分必要的。
如果不需要基类对派生类及对象进行操作,则不能定义虚函数,因为这样会增加内存开销.当类里面有定义虚函数的时候,编译器会给类添加一个虚函数表,里面来存放虚函数指针,这样就会增加类的存储空间.所以,只有当一个类被用来作为基类的时候,才把析构函数写成虚函数.
3、const关键字对C++成员函数的修饰
const对C++成员函数的修饰分为三种:1. 修饰参数;2. 修饰返回值;3. 修饰this指针
3.1 对函数参数的修饰。
- const只能用来修饰输入参数。输出型参数不能用const来修饰。
- 输入参数采用“指针传递”,那么加const修饰可以防止意外地改动该指针,起到保护作用。
- 如果输入参数采用“值传递”,函数将产生临时变量(局部变量),复制该参数的值并且压入函数栈。函数中使用该参数时,访问的是函数栈中临时变量的值,原变量无需保护,所以不要加const修饰。
- 基本变量类型的参数作为“值传递”的输入参数,无需采用引用。自定义变量类型(class类型,struct类型)参数作为“值传递”的输入参数,最好采用"const+引用"格式,即 void func(const A& a)。原因是自定义变量类型作为值传递时,设计创建临时变量,构造,复制,析构,这些过程很消耗时间。
3.2 对返回值的修饰。
这个应用比较少。大部分返回值采用的时“值传递”。如果将返回值修饰为const,那么接收返回值的变量也必须定义为const。
3.3 对this指针的修饰。
- 我们知道,c++成员函数在编译时候,会传入一个this指针,指向实例本身。这个this指针默认实际上是个顶层指针。即如果有classA,那么这个指针其实类似如下的定义:
classA * const this;
即this指针指向实例本身并且不可以修改,但可以通过this指针修改其指向的成员变量。在成员函数内访问成员变量m_var,实际上时如下形式方位的:
this.m_var;
- 如果我们设计一个成员函数时,不想让其修改成员变量,那么就应该将this指针定义为底层指针。c++定义的方式就是在函数签名后面加上const,即
void func(const A& a, int b, const int* c, int* d)const;
显然,上述成员函数中,a为const引用传递,不可以改变原值;b为值传递;c为const指针传递,不可改变原值;d为输出参数,可以改变原值。而该函数为const成员函数,不可以修改成员变量值
3.4 以下是const成员函数注意的几点:
- const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数.即对于class A,有
const A a;
那么a只能访问A的const成员函数。而对于:
A b;
b可以访问任何成员函数。
- const对象的成员变量不可以修改。
- mutable修饰的成员变量,在任何情况下都可以修改。也就是说,const成员函数也可以修改mutable修饰的成员变量。c++很shit的地方就是mutable和friendly这样的特性,很乱。
- const成员函数可以访问const成员变量和非const成员变量,但不能修改任何变量。检查发生在编译时。
- 非const成员函数可以访问非const对象的非const数据成员、const数据成员,但不可以访问const对象的任意数据成员。
- const成员函数只是用于非静态成员函数,不能用于静态成员函数。
- const成员函数的const修饰不仅在函数声明中要加(包括内联函数),在类外定义出也要加。
- 作为一种良好的编程风格,在声明一个成员函数时,若该成员函数并不对数据成员进行修改操作,应尽可能将该成员函数声明为const 成员函数。
4、隐式类型转换
《C++ Primer》中提到:
“可以用 单个形参来调用 的构造函数定义了从 形参类型 到 该类类型 的一个隐式转换。”
这里应该注意的是, “可以用单个形参进行调用” 并不是指构造函数只能有一个形参,而是它可以有多个形参,但那些形参都是有默认实参的。
**那么,什么是“隐式转换”呢? **
——上面这句话也说了,是从 构造函数形参类型 到 该类类型 的一个编译器的自动转换。
总结一下:
- 可以使用一个实参进行调用,不是指构造函数只能有一个形参。
- 隐式类类型转换容易引起错误,除非你有明确理由使用隐式类类型转换,否则,将可以用一个实参进行调用的构造函数都声明为explicit。
- explicit只能用于类内部构造函数的声明。它虽然能避免隐式类型转换带来的问题,但需要用户能够显式创建临时对象(对用户提出了要求)。
5、C++对象模型:对象内存布局详解
6、四种转换
en.cppreference.com/w/cpp/langu…
6.1 const_cast
const_cast顾名思义,用来将对象的常量属性转除,使常量可以被修改。const_cast(varible)中的type必须是指针,引用,或者指向对象类型成员的指针。
6.2 dynamic_cast
用来处理一种“安全向下转换”,当我们将父类指针指向一个new出来的子类A对象时,如果该父类有多个不同子类(class A,class B,),那么可以使用dynamic_cast将该指针类型安全转换为A*,如果使用强转操作符或者下面介绍的static_cast,那么将其转换为B*理论上也是可以的,但是使用上就会有错误:比如,类A有的成员函数,类B没有。
6.3 reinterpret_cast
是特意用于底层的强制转型,导致实现依赖(就是说,不可移植)的结果,例如,将一个指针转型为一个整数。这样的强制类型在底层代码以外应该极为罕见。操作结果只是简单的从一个指针到别的指针的值的二进制拷贝。在类型之间指向的内容不做任何类型的检查和转换。
6.4 static_cast
static_cast用来处理隐式转换,等同于C语言中的(NewType)Expression强转,它可以将int转为float,也可以将char转为int,将指向基类的指针转为一个指向子类的指针,同时可以将non-const转为const对象,但是它不能将一个const对象转为non-const(这个是const_cast的功能)。
//基本数据类型的转换
int intValue = 10;
double doubleValue = static_cast<double>(intValue);
//子类指针向父类指针的转换
class Base { /*...*/ };
class Derived : public Base { /*...*/ };
Derived derivedObj;
Base* basePtr = static_cast<Base*>(&derivedObj);
//指针和整数之间的转换
int* intPtr = reinterpret_cast<int*>(0x1234);
uintptr_t addr = reinterpret_cast<uintptr_t>(intPtr);
7、static_cast和dynamic_cast
7.1 static_cast
用法:static_cast < type-id > ( exdivssion ) 该运算符把exdivssion转换为type-id类型,但没有运行时类型检查来保证转换的安全性。它主要有如下几种用法: ①用于类层次结构中基类和子类之间指针或引用的转换。 进行上行转换(把子类的指针或引用转换成基类表示)是安全的; 进行下行转换(把基类指针或引用转换成子类表示)时,由于没有动态类型检查,所以是不安全的。 ②用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。 ③把空指针转换成目标类型的空指针。 ④把任何类型的表达式转换成void类型。
注意:static_cast不能转换掉exdivssion的const、volitale、或者__unaligned属性。
7.2 dynamic_cast
用法:dynamic_cast < type-id > ( exdivssion ) 该运算符把exdivssion转换成type-id类型的对象。Type-id必须是类的指针、类的引用或者void *; 如果type-id是类指针类型,那么exdivssion也必须是一个指针,如果type-id是一个引用,那么exdivssion也必须是一个引用。
dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。 在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的; 在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。
8、delete this 合法吗?
合法,但:
-
必须保证 this 对象是通过 new(不是 new[]、不是 placement new、不是栈上、不是全局、不是其他对象成员)分配的
-
必须保证调用 delete this 的成员函数是最后一个调用 this 的成员函数
-
必须保证成员函数的 delete this 后面没有调用 this 了
-
必须保证 delete this 后没有人使用了
9、new/delete与malloc/free区别
a.属性
new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持c。
b.参数
使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。
c.返回类型
new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
e. 分配失败
new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。
f.自定义类型
new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。
malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
g.重载
C++允许重载new/delete操作符,特别的,布局new的就不需要为对象分配内存,而是指定了一个地址作为内存起始区域,new在这段内存上为对象调用构造函数完成初始化工作,并返回此地址。而malloc不允许重载。
h.内存区域
new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中。
PS:
在C++中,内存区分为5个区,分别是堆、栈、自由存储区、全局/静态存储区、常量存储区;
在C中,C内存区分为堆、栈、全局/静态存储区、常量存储区;
10、类的拷贝构造函数调用时机
- 当使用一个已经存在的对象初始化另一个同类型的新对象时;
- 当函数参数(实参和形参)的类型都是对象,形参与实参结合时(实参初始化形参) ;
- 当函数的返回值是对象,执行return语句时_(编译器有优化)。
11、类的赋值运算符函数实现四部曲
-
考虑自复制
-
回收原本的堆空间
-
深拷贝(开辟空间再复制)以及其他的数据成员的复制
-
返回*this
Computer & operator=(const Computer & rhs){
if(this != &rhs){
delete [] _brand;
this->_brand = new char[strlen(rhs._brand) + 1]();
strcpy(_brand,rhs._brand);
this->_price = rhs._price;
}
return *this;
}
12、类的析构函数调用时机
- 栈对象生命周期结束时 ,会自动调用析构函数
- 全局对象在main函数退出时 ,会自动调用析构函数
- 静态对象在main函数退出时 ,会自动调用析构函数
- 堆对象当执行delete表达式时 ,会自动调用析构函数
13、单例对象的创建及自动释放的四种方式
单例对象的创建
- 将构造、拷贝构造、赋值运算符函数私有化 (确保不使用拷贝构造、赋值运算符函数,使用delete将它们从类中明确“删除”(C++11))
- 提供一个静态的本类指针类型的数据成员来存储生成的单例对象,将其初始化为nullptr
- 在静态成员函数中创建堆上的对象,利用静态指针数据成员做判断,确保只创建一次 (需要默认构造函数可以使用default简洁地显式提供(C++11))
- 定义成员函数实现数据成员的自定义
- 提供静态的成员函数用以回收堆对象本身的空间 (非改动析构函数,析构函数是用来回收对象的数据成员申请的堆空间)
#include <string.h>
#include <iostream>
using std::cout;
using std::endl;
class Computer{
public:
static Computer * getInstance(){
if(_pInstance == nullptr){
_pInstance = new Computer("App",10000);
}
return _pInstance;
}
void init(const char * brand,double price){
if(_brand){
delete [] _brand;
}
_brand = new char[strlen(brand) + 1]();
strcpy(_brand,brand);
_price = price;
}
static void destroy(){
if(_pInstance){
delete _pInstance;
_pInstance = nullptr;
}
cout << "堆上的对象空间被回收" << endl;
}
void print(){
cout << "brand:" << _brand << endl;
cout << "price:" << _price << endl;
}
private:
Computer(const Computer &) = delete;
Computer& operator=(const Computer &) = delete;
Computer() = default;
Computer(const char * brand,double price)
: _brand(new char[strlen(brand) + 1]())
, _price(price)
{
strcpy(_brand,brand);
cout << "Computer(const char*, double)" << endl;
}
~Computer(){
if(_brand){
delete [] _brand;
_brand = nullptr;
}
cout << "~Computer()" << endl;
}
private:
char * _brand;
double _price;
static Computer * _pInstance;
};
Computer * Computer::_pInstance = nullptr;
void test0(){
Computer::getInstance()->init("ASUS",7000);
Computer::getInstance()->print();
Computer::destroy();
}
int main(void){
test0();
return 0;
}
自动释放的四种方式
方式一:定义一个类托管指向单例对象的指针
方式二:在单例类中定义内部类AutoRelease,直接管理 _pInstance
方式三:atexit函数 + destroy函数
-
保留单例类的destroy函数
-
用atexit注册destroy函数,进程终结时被注册过的函数被调用,完成回收
-
_pInstance初始化有两种方式(饱汉式、饿汉式)
#include <iostream>
using std::cout;
using std::endl;
class Singleton{
public:
static Singleton * getInstance(){
//当多个线程同时进入if语句,可能创建出多个堆对象
//但是只有一个收到了_pInstance的管辖
//其他的都被泄漏
if(_pInstance == nullptr){
//在第一次创建单例对象时注册回收函数
atexit(destroy);
_pInstance = new Singleton(1,2);
}
return _pInstance;
}
void init(int x,int y){
_ix = x;
_iy = y;
}
void print(){
cout << "(" << _ix << ","
<< _iy << ")" << endl;
}
Singleton(const Singleton &) = delete;
Singleton & operator=(const Singleton &) = delete;
static void destroy(){
if(_pInstance){
delete _pInstance;
_pInstance = nullptr;
}
cout << "堆上的单例对象回收啦" << endl;
}
private:
Singleton(){
cout << "Singleton()" << endl;
}
Singleton(int x,int y)
: _ix(x)
, _iy(y)
{
cout << "Singleton(int,int)" << endl;
}
~Singleton(){
cout << "~Singleton()" << endl;
}
private:
int _ix;
int _iy;
static Singleton * _pInstance;
};
//饱汉式(懒汉式),懒加载,不到使用该对象就不创建
/* Singleton * Singleton::_pInstance = nullptr; */
//饿汉式,最开始就把对象创建完毕
Singleton * Singleton::_pInstance = getInstance();
void test0(){
Singleton::getInstance()->init(100,200);
Singleton::getInstance()->print();
Singleton::getInstance()->init(500,600);
Singleton::getInstance()->print();
Singleton::destroy();
}
int main(void){
test0();
return 0;
}
方式四:atexit函数 + pthread_once函数(Linux平台相关)
-
单例类添加静态数据成员 static pthread_once_t _once
-
单例类另外创建一个初始化的成员函数
-
在原本的接口函数getInstance函数中使用pthread_once函数
(第一个参数:指向静态数据成员_once的指针, 第二个参数:新的初始化函数的函数名)
-
编译指令加上多线程参数 -lpthread
-
注意
(1)不允许在类外手动调用destroy函数,如果手动回收了单例对象,之后再通过getInstance函数无法创建出单例对象
(2)不允许在类外手动调用init_r函数,否则会创建出多个堆上的对象
#include <pthread.h>
#include <iostream>
using std::cout;
using std::endl;
class Singleton{
public:
static Singleton * getInstance(){
//确保实际创建单例对象的函数init_r只会被调用一次
pthread_once(&_once,init_r);
return _pInstance;
}
static void init_r(){
_pInstance = new Singleton();
atexit(destroy);
}
void init(int x,int y){
_ix = x;
_iy = y;
}
void print(){
cout << "(" << _ix << ","
<< _iy << ")" << endl;
}
Singleton(const Singleton &) = delete;
Singleton & operator=(const Singleton &) = delete;
private:
static void destroy(){
if(_pInstance){
delete _pInstance;
_pInstance = nullptr;
}
cout << "堆上的单例对象回收啦" << endl;
}
Singleton(){
cout << "Singleton()" << endl;
}
Singleton(int x,int y)
: _ix(x)
, _iy(y)
{
cout << "Singleton(int,int)" << endl;
}
~Singleton(){
cout << "~Singleton()" << endl;
}
private:
int _ix;
int _iy;
static Singleton * _pInstance;
static pthread_once_t _once;
};
Singleton * Singleton::_pInstance = nullptr;
pthread_once_t Singleton::_once = PTHREAD_ONCE_INIT;
void test0(){
Singleton::getInstance()->init(100,200);
Singleton::getInstance()->print();
Singleton::getInstance()->init(500,600);
Singleton::getInstance()->print();
}
int main(void){
test0();
return 0;
}
14、vector 和 malloc 底层原理实现
15、野指针与悬空指针
1. 什么是野指针(wild pointer)?
A pointer in c which has not been initialized is known as wild pointer.
野指针(wild pointer)就是没有被初始化过的指针。
o foo1.c
1 int main(int argc, char *argv[])
2 {
3 int *p;
4 return (*p & 0x7f); /* XXX: p is a wild pointer */
5 }
如果用"gcc -Wall"编译, 会出现如下警告:
1 $ gcc -Wall -g -m32 -o foo foo.c
2 foo.c: In function ‘main’:
3 foo.c:4:10: warning: ‘p’ is used uninitialized in this function [-Wuninitialized]
4 return (*p & 0x7f); /* XXX: p is a wild pointer */
5 ^
2. 什么是悬空指针(dangling pointer)?
If a pointer still references the original memory after it has been freed, it is called a dangling pointer.
悬空指针是指针最初指向的内存已经被释放了的一种指针。 典型的悬空指针看起来是这样的,(图片来源是这里)
如果两个指针(p1和p2)指向同一块内存区域, 那么free(p1)后,p1和p2都成为悬空指针。如果进一步将p1设置为NULL, 那么p2还是悬空指针。诚然,使用p1会导致非法内存访问,但是使用p2却会出现无法预料的结果,可谓防不胜防。例如:
o foo2.c
1 #include <stdlib.h>
2 int main(int argc, char *argv[])
3 {
4 int *p1 = (int *)malloc(sizeof (int));
5 int *p2 = p1; /* p2 and p1 are pointing to the same memory */
6 free(p1); /* p1 is a dangling pointer, so is p2 */
7 p1 = NULL; /* p1 is not a dangling pointer any more */
8 return (*p2 & 0x7f); /* p2 is still a dangling pointer */
9 }
3. 使用野指针和悬空指针的危害
无论是野指针还是悬空指针,都是指向无效内存区域(这里的无效指的是"不安全不可控")的指针。 访问"不安全可控"(invalid)的内存区域将导致"Undefined Behavior"。
4. 如何避免使用野指针和悬空指针?
如何避免使用野指针? 好办! 养成在定义指针后且在使用之前完成初始化的习惯就好。
然而,如何避免使用悬空指针,就比较麻烦了。 Solaris引入了ADI(Application Data Integrity)技术避免访问已经释放的内存区域,例如: 最新的SPARC平台已经支持KADI,一旦访问某个已经释放掉的内核内存区域,就会引发操作系统panic。这里简单介绍一下什么是ADI。
ADI (Application Data Integrity) is a software layer built on
MCD (Memory Corruption Detection), a SPARC hardware feature that provides
statistical protection against memory corruption errors such as buffer
overflows, use-after-frees, and use-after-reallocs.
KADI allows kernel memory to use ADI.
这有效的避免了foo2.c示例代码中p2变成悬空指针还被使用的情况。 那么问题来了,如果没有ADI/KADI这种高大上的技术,如何避免使用悬空指针?
办法还是有的,直接避免不了就间接避免,那就是所谓的智能指针(smart pointer)。智能指针的本质是使用引用计数(reference counting)来延迟对指针的释放。
16、虚析构函数
虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。虚析构函数使用
class Shape
{
public:
Shape(); // 构造函数不能是虚函数
virtual double calcArea();
virtual ~Shape(); // 虚析构函数
};
class Circle : public Shape // 圆形类
{
public:
virtual double calcArea();
...
};
int main()
{
Shape * shape1 = new Circle(4.0);
shape1->calcArea();
delete shape1; // 因为Shape有虚析构函数,所以delete释放内存时,先调用子类析构函数,再调用基类析构函数,防止内存泄漏。
shape1 = NULL;
return 0;
}
虚函数、纯虚函数
-
类里如果声明了虚函数,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被覆盖,这样的话,这样编译器就可以使用后期绑定来达到多态了。纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里去实现。
-
虚函数在子类里面也可以不重载的;但纯虚函数必须在子类去实现。
-
虚函数的类用于 “实作继承”,继承接口的同时也继承了父类的实现。当然大家也可以完成自己的实现。纯虚函数关注的是接口的统一性,实现由子类完成。
-
带纯虚函数的类叫抽象类,这种类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。抽象类和大家口头常说的虚基类还是有区别的,在 C# 中用 abstract 定义抽象类,而在 C++ 中有抽象类的概念,但是没有这个关键字。抽象类被继承后,子类可以继续是抽象类,也可以是普通类,而虚基类,是含有纯虚函数的类,它如果被继承,那么子类就必须实现虚基类里面的所有纯虚函数,其子类不能是抽象类。
虚函数指针、虚函数表
-
虚函数指针:在含有虚函数类的对象中,指向虚函数表,在运行时确定。
-
虚函数表:在程序只读数据段(
.rodata section,见:目标文件存储结构),存放虚函数指针,如果派生类实现了基类的某个虚函数,则在虚表中覆盖原本基类的那个虚函数指针,在编译时根据类的声明创建。
虚继承
虚继承用于解决多继承条件下的菱形继承问题(浪费存储空间、存在二义性)。
底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。
实际上,vbptr 指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。
虚继承、虚函数
-
相同之处:都利用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)
-
不同之处:
-
- 虚继承
-
-
- 虚基类依旧存在继承类中,只占用存储空间
-
-
-
- 虚基类表存储的是虚基类相对直接继承类的偏移
-
-
- 虚函数
-
-
- 虚函数不占用存储空间
-
-
-
- 虚函数表存储的是虚函数地址
-