文中涉及语法等都是C++11相关语法。
参考了,文中3.1,3.2
,4.1
完全来源于该文。
后续有时间继续更新。
emsp;C++程序的内存模型包含:
可以使用下面两个分别查看简略的内存模型和比较详细的内存模型:
size filename
size --format=sysv filename
$ size cpptext data bss dec hex filename3075 640 280 3995 f9b cpp
$ size --format=sysv cpp
cpp :
section size addr
.interp 28 4194872
.note.ABI-tag 32 4194900
.u.build-id 36 4194932
.gnu.hash 48 4194968
.dynsym 360 4195016
.dynstr 376 4195376
.gnu.version 30 4195752
.gnu.version_r 64 4195784
.rela.dyn 48 4195848
.rela.plt 216 4195896
.init 26 4196112
.plt 160 4196144
. 8 4196304
.text 994 4196320
.fini 9 4197316
.rodata 64 4197328
.eh_frame_hdr 108 4197392
.eh_frame 468 4197504
.init_array 24 6299120
.fini_array 8 6299144
.jcr 8 6299152
.dynamic 480 6299160
.got 8 6299640
.got.plt 96 6299648
.data 16 6299744
.bss 280 6299776
ment 36 0
.debug_aranges 128 0
.debug_info 37413 0
.debug_abbrev 3498 0
.debug_line 2003 0
.debug_str 12648 0
.debug_ranges 64 0
Total 59785
二者速度对比:从原理上来说由于后缀型操作需要维护一个备份相对前缀多一部分指令,因此要慢一些。但在实际使用用C++的内置类型前缀和后缀的速度差异不大,唯一需要注意的是自定义类型重载的前后缀操作在性能开销性能上可能有明显的差异。
下面是内置int类型执行1000万次前缀和后缀操作的耗时:
j=i++ cost time is 6.91351e-07
j=++i cost time is 6.88428e-07
static修饰的变量作为静态变量,如果未经过初始化会存储在bss段,自动初始化为0,,如果经过初始化会存储在data段。
静态局部变量一般指的是函数中的静态变量,对于局部静态变量需要注意的是:
void func()
{static int cnt = 5;++cnt;cout<<"cnt = "<<cnt<<endl;
}void local_static_test()
{for(int i = 0;i < 3;i ++){func();}
}
输出为:
cnt = 6
cnt = 7
cnt = 8
静态全局变量对应的是全局变量,全局变量可以使用extern
关键字表明外部可见,静态全局变量在本文件中的行为和普通的全局变量相同唯一的区别是在其他文件中不可见。
static int global_cnt = 4;
void global_func()
{global_cnt ++;cout<<"cnt = "<<global_cnt<<endl;
}void global_static_test()
{for(int i = 0;i < 3;i ++){global_func();}
}
输出为
cnt = 5
cnt = 6
cnt = 7
类的静态成员变量就是在普通的成员变量之前添加static
关键字。需要注意的是:
typename classname::static_member=value
初始化,比如int mystring::len=0
;.h
文件中初始化,会报重复定义的错误;class static_class
{
public:static static_class member; //正确//const static string msg=string("wrong"); //错误static string msg;const static int len = 4;const static string info;
};
const string info = string("i am a static const member");
string static_class::msg = string("i am static class");
void static_member_test()
{cout<<"before instanced:"<<static_class::msg<<endl;static_class obj1;static_class obj2;obj1.msg = string("i am obj1");obj2.msg = string("i am obj2");cout<<"msg from static_class"<<static_class::msg<<endl;cout<<"msg from obj1:"<<obj1.msg<<endl;cout<<"msg from obj2:"<<obj2.msg<<endl;
}
输出:
before instanced:i am static class
msg from static_classi am obj2
msg from obj1:i am obj2
msg from obj2:i am obj2
静态成员函数和静态成员变量类似,声明使用static
关键字修饰即可,其属于整个类而不是某个实例。
virtual
。class static_class
{
public:int size; static static_class member; //正确//const static string msg=string("wrong"); //错误static string msg;const static int len = 4;static void static_func(string info=msg);//static void static_func_II(int sz=size); //错误void non_static_func();
};
void static_class::non_static_func()
{static_func();msg = string("nonstatic");
}void static_class::static_func(string info)
{//size = 10; //错误msg = info; //正确//non_static_func(); //错误
}
模板中的静态成员的所属关系是模板的实例化类。比如下面的例子,所有template_static_class<int>
共享一个len
,所有的template_static_class<long>
共享一个len
。
template<typename T>
class template_static_class
{
public:static T len;
};
template<typename T>
T template_static_class<T>::len = 3;void template_static_test()
{cout<<"int before instanced:"<<template_static_class<int>::len<<endl;template_static_class<int> int1;template_static_class<int> int2;int1.len = 4;int2.len = 5;cout<<"int after instanced:"<<template_static_class<int>::len<<endl;cout<<"int len from int 1:"<<int1.len<<endl;cout<<"int len from int 2:"<<int2.len<<endl;cout<<endl;cout<<"long before instanced:"<<template_static_class<long>::len<<endl;template_static_class<long> long1;template_static_class<long> long2;long1.len = 1100;long2.len = 1200;cout<<"long after instanced:"<<template_static_class<long>::len<<endl;cout<<"long len from long 1:"<<long1.len<<endl;cout<<"long len from long 2:"<<long2.len<<endl;
}
int before instanced:3
int after instanced:5
int len from int 1:5
int len from int 2:5long before instanced:3
long after instanced:1200
long len from long 1:1200
long len from long 2:1200
const类型声明直接在类型前加const即可,指针类型可以同时添加两个const,如const int * const val;
const int val = 10;//const int no; //错误,未初始化const int no(val);const int *p = 0;int * const ptr = 0;const int * const pp = 0;
对于一般全局变量,我们如果需要在其他文件中使用需要在当前文件中使用extern
声明过便可以使用,const
类型的全局变量需要在定义处和声明处都加上extern
关键字。
//1.cpp
extern const int rst = 0;
int snd = 2;
//2.cpp
extern const int rst;
extern int snd;
void func(){
snd = rst;
}
const
和引用的组合需要注意:
const
的值不能绑定非const
的引用;const
引用可以绑定立即数;const
引用可以绑定表达式;const
引用可以绑定不同类型的可以隐式转换的变量。 其实理由也很容易理解:被const
修饰的值只能访问,不能修改。
const int value = 10;const int& val_ref = val;//int & ref = val; //错误//int &ref = 42; //错误const int &const_ref = 43;//int &add_ref = val + value; //错误const int &add_ref = val + value;//long & long_ref = val; //错误const long &long_ref = val;
在这之前先提醒下,c++11中const
整形变量可以作为数组声明的长度,之前版本的c++不支持。
const int size = 10;int array[size];
const和指针的组合其实不是很难理解记住,const
右边是什么什么就不能改变的原则就可以了,比如对于int *const p;
,const
右边是p
,而p
是指针就表示为常指针。const int * p;
,const
右边是int
则表示值不可变。
const typename * p;
指向常量的指针。指针可变,值不可变。typename * const p;
指向变量的常指针。指针不可变,值可变。const typename * const p;
指向常量的常指针。指针和值都不可变。 int len = 0;const int * p1;int * const p2 = &len;const int * const p3 = &len;
类中的const
主要分为常量成员变量,const成员函数。
const
成员变量定义和普通的成员变量相同,除了静态成员变量初始化时只能使用参数列表初始化。
非const
成员函数和const
成员函数可以构成重载(const
成员函数表示成员函数中的变脸不可修改)。
class const_class{
public:const int len;const_class():len(2){}void print(){cout<<"void print()"<<endl;}void print() const{cout<<"void print() const"<<endl;}
};void const_class_test()
{const_class cls;const const_class cls1;cls.print();cls1.print();
}
输出:
void print()
void print() const
当const
修饰函数返回值的时候只有指针时相对有具体意义。
const int * func(){return 0;
}const int & func2(){return 0;
}
//int * p = func(); //错误const int * p = func();//int& a = func2(); //错误const int& b = func2();
另外const
修饰函数参数和普通的const
含义等同。
c语言中的const并不是严格不可变的那么c++中的const是不是严格不可变的?下面针对普通的const
类型和类中的const
成员变量进行测试。
对const
取地址,尝试修改const
的值:
const int val = 10;//val = 12; //错误,直接修改int *const_ptr = const_cast<int *>(&val);int *ptr = (int*)&val;*ptr = 12;*const_ptr = 333;cout<<"const value:"<< val<<endl;cout<< "const_cast:"<<*const_ptr<<endl;cout<<"address convert:"<<*ptr<<endl;
从输出可以看到使用const_cast
和普通的类型转换取到的地址是同一块地址,原来的常量val
值并未改变,猜测大概的原因是:常量本身存储与常量区rodata
,在用户对常量取地址时会在栈上新建一个该常量一个非常量的副本供用户操作,而实际的常量值依然安全:
const value:10
const_cast:333
address convert:333
emsp; 对类中的const
成员变量进行修改尝试。
class const_class{public:const int len;};const_class obj;//obj.len = 0; //错误int * const_ptr = const_cast<int*>(&obj.len);int * ptr = (int*)(&obj.len);*ptr = 12;*const_ptr = 333;cout<<"const value:"<< obj.len<<endl;cout<< "const_cast:"<<*const_ptr<<endl;cout<<"address convert:"<<*ptr<<endl;
输出,修改成功:
const value:333
const_cast:333
address convert:333
因此可以认为类成员中的const
关键字不同与普通的const
,类中的可以修改,普通的无法修改,即便修改修改的也不是整整的const
。要解释这个可以从内存模型上来说(下面是个人的猜测):普通的const
存储与rodata
,readonly存储区不可能修改,但是常量作为变量又不能有不能取地址的特权,因此当对普通常量取地址时编译器就在栈上新建一个该常量的副本,因此修改后无效。而对于类内的成员变量,由于常量不同与静态成员可以特殊存储,因为常量对于每一个实例都存在副本,将每一个示例对应的常量存储与特殊区域不太现实,因此类的常量成员变量和类本身存储在一起组成类,因此可以读取内存地址修改(对于c,c++能够读到地址没有什么不能修改)。
指针本身是变量,存储的是指向的变量的地址。引用本身是别名,本身不能独立存在。
指针和引用最大的区别是指针可以修改而引用不能修改。需要注意的是下面的语句中p=value
是将value
的值赋值给val
。
根本原理是程序进行编译时将指针和引用添加到符号表中,符号表上指针所对应的是指针变量自身的地址,引用符号表上对应的地址是引用所指向的对象的地址,且符号表固定后便不可修改,因此一般使用引用是安全的,可以认为引用是不可修改的指针。
int val = 0;int value = 10;int &p = val;p = value;
区别:
int val = 0;
int ** ptr
int &&p = val; //非法
C++中动态内存分配可以使用malloc
和new
,二者都是在堆上分配内存。二者不同有:
new
分配的内存要使用delete
释放,malloc
分配的内存要使用free
使用,不能交叉使用;malloc
只负责分配内存,需要用户自己初始化,new
除了分配内存可以指定初始化方式,未指定则调用默认构造函数;malloc
开辟内存时传入的是字节数一般为size*sizeof(typename)
,而new
直接传入需要的size即可;malloc
分配的内存返回的是void*
,需要强制类型转换为目标类型,new
返回的内存自带类型;malloc
分配失败返回NULL
,new
分配失败抛出bad_alloc
异常;malloc
只有一个版本,而new``有普通的
new,
nothrow的
new,
const new和定位
new```。 int *norm = new int(3); //普通new,出错抛出异常bad_allocint *no_throw = new(std::nothrow)int(3); //nothrow版本,失败不抛出异常const int *const_p = new int(3);int * const p_const = new int(3);int *buf = new int[1000];virtual_class* p_cls = new (buf)virtual_class(); //并不额外分配内存只在已经分配好的内存上创建相关的类p_cls->~virtual_class(); //需要显示的调用析构函数销毁delete [] buf;
需要注意的是定位new
即placement new
实际上并不分配内存,只是在已经分配好的内存上创建相关的数据,销毁时只能用delete
销毁缓冲区的内存,具体的对象数据需要显示的调用析构函数。
可以参考一个关于placement new的问题?中关于placement new
的回答。
另外realloc
在c++中也可用但是不建议因为会出现浅拷贝的问题,同样的问题发生于memcpy
等c相关的拷贝函数。
calloc
也可以分配内存,区别是分配完自动填0。
&emsp;&emsps;alloca
:在栈上申请内存。程序在出栈的时候,会自动释放内存。但是需要注意的是,alloca
不具可移植性, 而且在没有传统堆栈的机器上很难实现。alloca 不宜使用在必须广泛移植的程序中。C99 中支持变长数组 (VLA),可以用来替代 alloca。
内存泄漏一般是指堆内存的泄漏,即程序在运行过程中动态申请的内存空间不再使用后没有及时释放,导致那块内存不能被再次使用。更广义的内存泄漏还包括未对系统资源的及时释放,比如句柄、socket等没有使用相应的函数释放掉,导致系统资源的浪费。解决方法:
参考C/C++内存泄漏及检测
智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。
share_ptr
使用引用计数实现多个share_ptr
共享一个对象,当最后一个资源的引用被销毁时会被释放。share_ptr
可以使用new,auto_ptr,unique_ptr,weak_ptr
构造,使用release()
销毁。
unique_ptr
实现对象的独占,同一时间一个unique_ptr
只能指向一个对象。可以通过move
转移所有权。当两个对象相互使用一个share_ptr
成员变量指向对方会造成循环应用问题,导致引用计数失效。
weak_ptr
是一种不控制对象生命周期的智能指针。weak_ptr
被设计为与 shared_ptr
共同工作,可以从一个 shared_ptr
或者另一个 weak_ptr
对象构造而来。weak_ptr
是为了配合 shared_ptr
而引入的一种智能指针,它更像是 shared_ptr
的一个助手而不是智能指针,因为它不具有普通指针的行为,没有重载 operator* 和 operator->
,因此取名为 weak,表明其是功能较弱的智能指针。它的最大作用在于协助 shared_ptr
工作,可获得资源的观测权,像旁观者那样观测资源的使用情况。观察者意味着 weak_ptr
只对 shared_ptr
进行引用,而不改变其引用计数,当被观察的 shared_ptr
失效后,相应的 weak_ptr
也相应失效。
智能指针陷阱:
内存对齐的原则:
#pragma pack(n)
指定对齐的字节数。class align1
{char a; //1short b; //2int c; //4long d; //8
};class align2
{char a;char b;
};class align4
{char a;char b;long c;
};
比如上面3个类的大小分别为16,2,16,拿align4举例,成员a和b的地址分别为&align4+1和&align4+2
,而c的地址为&align+8
,即b和c之间有6个字节的补齐。
另外可以使用冒号指定数据所占位数,下面类大小为8,总共是10位1个字节,但是要按照8来对齐:
class align3
{char a:1;short b:2;int c:3;long d:4;
};
类型 | 长度(字节) |
---|---|
char | 1 |
short int | 2 |
int | 4 |
unsigned int | 4 |
long | 8(机器相关,32位机为4) |
long long | 8 |
unsigned long | 8(机器相关,32位机为4) |
float | 4 |
double | 8 |
指针 | 8(机器相关,32位机为4) |
测试输出
char 1
short int 2
int 4
unsigned int 4
long 8
long long 8
unsigned long 8
float 4
double 8
void * 8
C++中的类型推断主要使用auto,decltype,typeid
,三者在不同情况下的作用不同行为不同。对于auto
:
auto
必须在定义时进行初始化;auto
定义序列变量必须能够推导为同一类型,如auto a=10,b=30.0, c='a';
在b处出错;const/volatile
,则去除相关语义;auto
带上&
,则不去除const
语义;auto
关键字推导类型为指针,带上&
则推导为数组;auto
;auto
并不是一个类型只是一个占位符,不能使用对类型进行操作的操作符处理。 decltype
类型推断相比于auto
能够保留const
等信息。
typeid
主要用于RTTI(Run-Time Type Identification,运行时类型识别),这部分我自己测试的结果和网上的博客有出入,以后再更新[TODO]
int val;int &val1 = val;const int val2 = val;const int &val3 = val;int *val4 = &val;father * f = new father();child *c = new child();father *ff = c;cout<<typeid(val).name()<<endl; //int icout<<typeid(val1).name()<<endl; //int icout<<typeid(val2).name()<<endl; //int icout<<typeid(val3).name()<<endl; //int icout<<typeid(val4).name()<<endl; //int* i cout<<typeid(tolower).name()<<endl;; //int (*)(int) Fiiecout<<typeid(f).name()<<endl; //father*cout<<typeid(c).name()<<endl; //child*cout<<typeid(ff).name()<<endl; //father*decltype(val) new_val = val; //intdecltype(val1) new_val1 = val; //int&decltype(val2) new_val2= val; //const intdecltype(val3) new_val3 = val; //const int &decltype(val4) new_val4 = &val; //int *decltype(f) f1 = f; //father*decltype(c) c1 = c; //child*decltype(ff) ff1 = c; //father*auto aut_val = val; //intauto aut_val1 = val; //intauto aut_val2 = val; //intauto aut_val3 = val; //intauto aut_val4 = &val; //int*auto f2 = f; //father*auto c2 = c; //child*auto ff2 = c; //child*
RTTI(Run-Time Type Identification,运行时类型识别),C++是一种静态类型语言。其数据类型是在编译期就确定的,不能在运行时更改。然而由于面向对象程序设计中多态性的要求,C++中的指针或引用(Reference)本身的类型,可能与它实际代表(指向或引用)的类型并不一致。有时我们需要将一个多态指针转换为其实际指向对象的类型,就需要知道运行时的类型信息,这就产生了运行时类型识别的要求。
C++中的RTTI一般使用typeid
和dymanic_cast
进行多态时的类型推断:
typeid
返回当前指针的真实类型,前提是类中的虚函数得到实现;dymanic_cast
将基类转换成子类,如果转换失败返回空,用来判断基类能否转换成子类。详细参考C++中的RTTI(转)
注意对能成功进行的含virtual方法的类,通过引用转换和指针转换后对virtual方法的调用不同:假设转换前为pref_class转换后为post_class,则通过引用转化调virtual方法实际是调用post_class中对应方法的;通过指针转换调post_class中virtual指针相对存放位置对应pref_class相对位置的指针对应的方法(若该位置指向不是方法,运行失败),形参不一致会自动补全(一般是乱码)或删减。
bad_cast
,dynamic_cast
的可抛出异常版本。
C++宏定义实质上是一种类似于函数的功能,但是其本身并不是函数只是简单的替换,更不存在类型检查的措施:
inline
唯一的区别是类型检查机制;#
或者##
将不再展开。### 1.11.2 常用的宏定义
#define str(x) #x
,令x=123
则str(x)
等同于123
;#define conv(x,y) x##y
,传入conv(a,1)
等同于变量a1
;#define tochar(x)
,传入a
等同于````a` ```;#ifndef FILE_H
#define FILE_H
//TODO:coding
#endif //FILE_H
#define mem_byte(addr) ( *( (byte *) (x) ) )
#define mem_word(addr) ( *( (word *) (x) ) )
#define offset( type, field ) sizeof( ((type *) 0)->field )
#define to_upper( c ) ( ((c) >= 'a' && (c) <= 'z') ? ((c) - 0x20) : (c) )
#define to_lowwer( c ) ( ((c) >= 'A' && (c) <= 'Z') ? ((c) + 0x20) : (c) )
#define byte_ptr( var ) ( (byte *) (void *) &(var) )
#define word_ptr( var ) ( (word *) (void *) &(var) )
#define dec_check( c ) ((c) >= ''0'' && (c) <= ''9'')
#define hex_check( c ) ( ((c) >= ''0'' && (c) <= ''9'') ||((c) >= ''A'' && (c) <= ''F'') ||((c) >= ''a'' && (c) <= ''f'') )
#define inc_sat(val) (val = ((val) + 1 > (val))?(val) + 1 : (val))
#deifne array_size(arr) (sizeof( (arr) ) / sizeof( (a[0]) ))
__LINE___ //代码出的行号%d
__FILE__ //执行代码的文件名%s
__DATE__ //日期%s
__TIME__ //时间%s
C++ 11 中的 Lambda 表达式用于定义并创建匿名的函数对象,以简化编程工作。Lambda 的语法形式如下:
[函数对象参数] (操作符重载函数参数) mutable 或 exception 声明 -> 返回值类型 {函数体}
函数对象:
操作符重载函数参数:标识重载的 () 操作符的参数,没有参数时,这部分可以省略。参数可以通过按值(如: (a, b))和按引用 (如: (&a, &b)) 两种
方式进行传递。
mutable 或 exception 声明:这部分可以省略。按值传递函数对象参数时,加上 mutable 修饰符后,可以修改传递进来的拷贝(注意是能修改拷贝,而不是值本身)。exception 声明用于指定函数抛出的异常,如抛出整数类型的异常,可以使用 throw(int)。
-> 返回值类型:标识函数返回值的类型,当返回值为 void,或者函数体中只有一处 return 的地方(此时编译器可以自动推断出返回值类型)
时,这部分可以省略。
函数体:标识函数的实现,这部分不能省略,但函数体可以为空。
[] (int x, int y) { return x + y; } // 隐式返回类型
[] (int& x) { ++x; } // 没有 return 语句 -> Lambda 函数的返回类型是 'void'
[] () { ++global_x; } // 没有参数,仅访问某个全局变量
[] { ++global_x; } // 与上一个相同,省略了 (操作符重载函数参数)
虚函数:
virtual
关键字声明,允许被子类重新定义的成员函数;virtual
关键字;纯虚函数:
=0
;class class1{
virtual func(); //虚函数
virtual func1() = 0; //纯虚函数
};
多态分为两种:静态多态和动态多态。
静态多态即函数重载,编译期决定了执行内容。
动态多态使用虚函数实现,即执行期才决定执行内容。多态的目的是为了接口重用,封装可以使得代码模块化,继承可以扩展已存在的代码。
多态的使用方法是:声明两个类分别为父子类,父类中存在虚函数,子类对该虚函数进行了重写,在通过基类指针或者引用调用该方法是对于不同的实例会自动调用自身的该方法。所以多态的关键是:
class virtual_class
{
public:virtual void run(){cout<<"i am the fater"<<endl;}
};class virtual_class_child : public virtual_class
{
public:virtual void run() override{cout<<"i am the kid"<<endl;}
};void vitual_test_func(virtual_class & cls)
{cls.run();
}void virtual_test()
{virtual_class father;virtual_class_child child;vitual_test_func(father);vitual_test_func(child);
}
输出:
i am the fater
i am the kid
另外override
关键字可以显式的在派生类中声明,哪些成员函数需要被重写,如果没被重写,则编译器会报错,是一种辅助手段。override
一般用于子类中。
linux下可以使用如下指令获得class文件得到类的结构:
g++ -fdump-class-hierarchy filename
class virtual_class
{
public:virtual void run(){cout<<"i am the fater"<<endl;}virtual void func1(){}virtual void func2(){}virtual void func3(){}};class virtual_class_child : public virtual_class
{
public:virtual void run() override{cout<<"i am the kid"<<endl;}virtual void func1(){}virtual void func2(){}
};
上述两个类的虚函数表为:
Vtable for virtual_class
virtual_class::_ZTV13virtual_class: 6u entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI13virtual_class)
16 (int (*)(...))virtual_class::run
24 (int (*)(...))virtual_class::func1
32 (int (*)(...))virtual_class::func2
40 (int (*)(...))virtual_class::func3
Vtable for virtual_class_child
virtual_class_child::_ZTV19virtual_class_child: 6u entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI19virtual_class_child)
16 (int (*)(...))virtual_class_child::run
24 (int (*)(...))virtual_class_child::func1
32 (int (*)(...))virtual_class_child::func2
40 (int (*)(...))virtual_class::func3
C++中每个对象都会维护一个虚函数表指针,而虚函数表属于类本身。继承关系中的虚函数表的结构:子类先继承父类的虚函数表,如果子类重写了相关方法则子类的虚函数表上会替换为子类的方法,如果子类定义了新的虚函数该函数会被添加到虚函数表上面。
在多继承的情况下,单个类会维护多个虚函数表。
class mother
{virtual void func1();
};class father
{virtual void func1();virtual void func2();
};class child: public father, public mother
{virtual void func2();virtual void func3();
};
Vtable for father
father::_ZTV6father: 4u entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI6father)
16 (int (*)(...))father::func1
24 (int (*)(...))father::func2
Vtable for mother
mother::_ZTV6mother: 3u entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI6mother)
16 (int (*)(...))mother::func1
Vtable for child
child::_ZTV5child: 8u entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI5child)
16 (int (*)(...))father::func1
24 (int (*)(...))child::func2
32 (int (*)(...))child::func3
40 (int (*)(...))-8
48 (int (*)(...))(& _ZTI5child)
56 (int (*)(...))mother::func1
拷贝函数被调用的三种情形:
浅拷贝:类中并未显示声明拷贝构造函数,导致拷贝时只是简单的赋值拷贝,对于指针等指向的资源出现多个对象拥有同一份资源的情况;
深拷贝:自定义拷贝函数,明确不同资源的拷贝方式,避免一份资源多个类共享。
一般如果普通的赋值操作能够保证两个类的资源不共享就不需要深拷贝,否则一定要深拷贝。
空类编译器回味其默认添加构造函数,拷贝构造函数,赋值函数,析构函数,这些函数只有在第一次被调用时,才会被编译器创建。空类其大小为1,类中函数本身不占空间。
class empty{};
class empty1{
public:empty1(){}empty1(const empty1 &){}empty1 & operator=(const empty1 &){}~empty1(){}
};
需要注意的是下面类的大小为8因为要维护一个虚函数表指针。
class empty2{
public:empty2(){}empty2(const empty2 &){}empty2 & operator=(const empty2 &){}~empty2(){}void func2(){}virtual void func3(){}
};
构造时,先构造基类再销毁派生类;销毁时,先销毁派生类再销毁基类。而对于多继承构造时按照继承的顺序构造,析构时按照继承的反顺序析构。
class base1
{
public:base1(){cout<<"base1 constructor"<<endl;}~base1(){cout<<"base1 deconstructor"<<endl;}
};class base2
{
public:base2(){cout<<"base2 constructor"<<endl;}~base2(){cout<<"base2 deconstructor"<<endl;}
};class base3
{
public:base3(){cout<<"base3 constructor"<<endl;}~base3(){cout<<"base3 deconstructor"<<endl;}
};class derived1:public base1, public base2, public base3
{
public:derived1(){cout<<"derived1 constructor"<<endl;}~derived1(){cout<<"derived1 deconstructor"<<endl;}
};
base1 constructor
base2 constructor
base3 constructor
derived1 constructor
derived1 deconstructor
base3 deconstructor
base2 deconstructor
base1 deconstructor
派生类数据成员初始化顺序:
this
指针是一个隐含于每一个非静态成员函数中的特殊指针。它指向调用该成员函数的那个对象。this
指针,然后调用成员函数,每次成员函数存取数据成员时,都隐式使用 this
指针。this
指针被隐含地声明为: ClassName *const this
,这意味着不能给 this
指针赋值;在 ClassName
类的 const
成员函数中,this
指针的类型为:const ClassName* const
,这说明不能对 this
指针所指向的这种对象是不可修改的(即不能对这种对象的数据成员进行赋值操作);this
并不是一个常规变量,而是个右值,所以不能取得 this
的地址(不能 &this
);this
指针解引用得到是该类型变量的一个引用;this
指针: list
。成员函数最终被编译成与对象无关的普通函数,除了成员变量,会丢失所有信息,所以在编译时要在成员函数中添加一个额外的参数,把当前对象的首地址传入,以此来关联成员函数和成员变量。
friend
友元可以使得指定的函数或者类可以访问目标类的私有数据或者成员函数。在使用友元时在目标类中声明相关函数或者类之前添加friend
关键字即可,而声明的位置是private
还是public
并无无别。
即便实现类之间的数据共享,减少系统开销,提高效率,但是要注意的是友元破坏了封装性,等同于特权。封装本身是为了隐藏数据,友元向部分函数或类提供了特权,使得其可以访问私有数据和成员。一般情况下不到万不得已不要使用友元,大部分场景都可以使用其他方式解决。友元使用比较频繁的场景是操作符重载和数据共享。
class friend_class
{int val;
public:friend_class():val(10){}int operator+(int value){cout<<"friend class operator "<<value + val<<endl;return value + val;}
};void friend_test()
{friend_class cls;int val = cls + 2;//int val2 = 2 + cls; //错误
}
从上面的例子可以看出直接使用成员函数重载无法保证语义,使用下面的友元便可以保证完整的语义。
class friend_class
{int val;
public:friend_class():val(10){}friend int operator+(friend_class& cls, int val);
};int operator+(friend_class & cls, int val)
{return cls.val + val;
}int operator+(int val, friend_class &cls)
{return cls + val;
}
move
和forward
语义详细见移动语义(move semantic)和完美转发(perfect forward)
凡是真正的存在内存当中,而不是寄存器当中的值就是左值,其余的都是右值。其实更通俗一点的说法就是:凡是取地址(&)操作可以成功的都是左值,其余都是右值。
对于左值的引用就是左值引用,而对于右值的引用就是右值引用。const
引用可以绑定左值和右值。
左值引用就是一般使用的引用T&
,右值引用为T&&
。
move
语义 move
语义能够将左值转换成右值。一般情况下构建类为了保证安全性会为类定义深拷贝构造函数,但是如果我们仅仅是根据现有数据构造类的话,依旧保持使用深拷贝构造函数的话性能就会大大折扣,因此move
语义提供了一种声明浅拷贝构造函数的方式,即移动拷贝构造函数,来提升性能。
class move_class
{int *data;int size;
public:move_class(int sz){size = sz;data = new int[size];memset(data, 0, sizeof(int) * size);}move_class(move_class& cls){size = cls.size;data = new int[size];memcpy(data, cls.data, size * sizeof(int));}move_class(move_class&& cls){size = cls.size;data = cls.data;cls.data = nullptr;}
}void move_test()
{move_class cls1;move_class cls2(std::move(cls1)); //cls1在之后不在被使用move_class cls3(move_class(400)); //一般不会这么做,演示而已
}
需要注意的是使用std::move
处理过的对象不能再使用了。另外一些编译器默认提供NROV优化的无法使用move
语义,因此需要添加编译器参数。RVO和NROV优化
forward
语义forward()则会保留参数的左右值类型,保证在跨函数传参时能够保证参数本身的左右值类型。
void func1(int val){}
void func2(int && val){func1(std::forward<int>(val));
}
构成通用引用有两个条件:
auto
声明;typedef
;decltype
。T& & -> T&
;T&& & -> T&
;T& && -> T&
;T&& && -> T&&
。const
、enum
、inline
替换 #define
)operator=
返回一个 reference to *this
(用于连锁赋值)operator=
中处理 “自我赋值”new
中使用 []
则 delete []
,new
中不使用 []
则 delete
)(T)expression
、T(expression)
;新式:const_cast<T>(expression)
、dynamic_cast<T>(expression)
、reinterpret_cast<T>(expression)
、static_cast<T>(expression)
、;尽量避免转型、注重效率避免 dynamic_casts、尽量设计成无需转型、可把转型封装成函数、宁可用新式转型)tr1::function
成员变量替换 virtual 函数,将继承体系内的 virtual 函数替换为另一个继承体系内的 virtual 函数)this->
指涉 base class templates 内的成员名称,或藉由一个明白写出的 “base class 资格修饰符” 完成)static_cast
、const_cast
、dynamic_cast
、reinterpret_cast
)&&
,||
和 ,
操作符(&&
与 ||
的重载会用 “函数调用语义” 取代 “骤死式语义”;,
的重载导致不能保证左侧表达式一定比右侧表达式更早被评估)new operator
、operator new
、placement new
、operator new[]
;delete operator
、operator delete
、destructor
、operator delete[]
)容器 | 底层数据结构 | 时间复杂度 | 有无序 | 可不可重复 | 其他 |
---|---|---|---|---|---|
array | 数组 | 随机读改 O(1) | 无序 | 可重复 | 支持随机访问 |
vector | 数组 | 随机读改、尾部插入、尾部删除 O(1) 头部插入、头部删除 O(n) | 无序 | 可重复 | 支持随机访问 |
deque | 双端队列 | 头尾插入、头尾删除 O(1) | 无序 | 可重复 | 一个中央控制器 + 多个缓冲区,支持首尾快速增删,支持随机访问 |
forward_list | 单向链表 | 插入、删除 O(1) | 无序 | 可重复 | 不支持随机访问 |
list | 双向链表 | 插入、删除 O(1) | 无序 | 可重复 | 不支持随机访问 |
stack | deque / list | 顶部插入、顶部删除 O(1) | 无序 | 可重复 | deque 或 list 封闭头端开口,不用 vector 的原因应该是容量大小有限制,扩容耗时 |
queue | deque / list | 尾部插入、头部删除 O(1) | 无序 | 可重复 | deque 或 list 封闭头端开口,不用 vector 的原因应该是容量大小有限制,扩容耗时 |
priority_queue | vector + max-heap | 插入、删除 O(log2n) | 有序 | 可重复 | vector容器+heap处理规则 |
set | 红黑树 | 插入、删除、查找 O(log2n) | 有序 | 不可重复 | |
multiset | 红黑树 | 插入、删除、查找 O(log2n) | 有序 | 可重复 | |
map | 红黑树 | 插入、删除、查找 O(log2n) | 有序 | 不可重复 | |
multimap | 红黑树 | 插入、删除、查找 O(log2n) | 有序 | 可重复 | |
unordered_set | 哈希表 | 插入、删除、查找 O(1) 最差 O(n) | 无序 | 不可重复 | |
unordered_multiset | 哈希表 | 插入、删除、查找 O(1) 最差 O(n) | 无序 | 可重复 | |
unordered_map | 哈希表 | 插入、删除、查找 O(1) 最差 O(n) | 无序 | 不可重复 | |
unordered_multimap | 哈希表 | 插入、删除、查找 O(1) 最差 O(n) | 无序 | 可重复 |
vector
本身是动态数组,如果未指定本身会分配大于当前需求的内存防止频繁分配内存造成的额外开销,这也体现为什么有成员函数size()
和capacity()
。另外vecotr
在不同编译器下增长方式不同,gcc
下每次增长为原来的2倍,vs的编译器cl
下每次增长为原来的1.5倍。
array
允许随机访问,其迭代器属于随机迭代器,其size()的结果总等于N,不支持分配器,是唯一一个无任何东西被指定为初值时,会被预初始化的容器,这意味着对于基础类型初值可能不明确。class array<>
是一个聚合体(不带用户提供的构造函数,没有private和protected的nonstatic数据成员,没有base类,有没有virtual 函数),这意味着保存所有元素的那个成员是public,然而C++并没有指定其名称,因此对该public成员的任何直接访问都会导致不可预期的行为,也绝对不可移植。
deque容器类与vector类似,支持随机访问和快速插入删除,它在容器中某一位置上的操作所花费的是线性时间。与vector不同的是,deque还支持从开始端插入数据:push_front()。
forward_list是一种能在常数时间内在任何位置插入和删除的顺序容器。Forward_list是单向链表。
list是由双向链表实现的,因此内存空间是不连续的。只能通过指针访问数据,所以list的随机存取非常没有效率,时间复杂度为o(n);但由于链表的特点,能高效地进行插入和删除。
auto,for,lambda,override, final, nullptr
,智能指针等;define和const
区别?define
是纯粹的替换,const
本身依然是一个变量;struct和class
区别?基本相同,唯一不同的是默认的访问控制:struct
默认public
,class
是private
;sizeof
和strlen
的区别?sizeof
计算字节大小,strlen
计算目标元素的个数(以’ ’为结尾),同时sizeof
能够计算处数组所占字节大小;volatile
关键字用处?volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问;ineline
和define
区别?inline
本身也是替换,相比宏定义多了类型检查;assert
?assert
是宏定义,是调试断言,assert
的作用是现计算表达式 expression ,如果其值为假(即为0),那么它先向stderr打印一条出错信息,然后通过调用 abort
来终止程序运行。调试结束后可以使用#define NDEBUG
失效该语句;#pragma pack(n)
指定内存以n
字节对齐,具体见内存对齐;extern "C"
包裹C代码即可;struct
有何不同?C语言中的struct
作为一种结构化数据存在,一般使用typedef
进行重命名,而C++中的struct
只是和class
默认访问控制不同而已;另外;union
的区别?uinion
在C中作为一种特殊的数据存在,而C++中基本的行为很像class
但是: union
不能包含protected
和private
成员;union
必须是static
的。explicit
的作用?防止隐式类型转换,避免初始化时复制;using
?尽可能的使用using
声明,而不是using
指示,后者容易导致名字污染;另外在子类中使用using base::base;
派生类会直接替子类构造调用基类的构造函数,即derived(args):base(args){}
;::
全局;classname::
类中,namespace::
命名空间中;int
类型,但是int
类型无法隐式转换成枚举类型;initializer_list
?initializer_list
一般使用于构造函数中可以使用列表初始化类,initializer_list
中只能是无法改变的常量,某种程度上等同于常数数组;delete this
是否合法?对象是通过new
获得,且在delete this
之后该对象完全不再使用的情况下合法;new
和delete
;delete
和default
的含义?delete
表示禁用该版本构造函数,default
表示自动生成该版本默认构造函数;final
语义?final
修饰类表示不希望被继承,修饰虚函数表示不希望被override
;const
和constexpr
的区别?const
某种程度上只是readonly
保证运行时不变性,而constexpr
保证了编译期的不可变;.i
文件(g++ -E),.i
文件经过编译器转换成汇编代码s
(g++ -S),’.s’汇编代码经过汇编器编译成.o
文件(g++ -c),链接器将’.o’文件链接成可执行文件(g++ -o),不同平台可执行文件不同,windows为PE格式文件,linux下位ELF格式文件;++
和后置++
?前置operator++()
,后置operator++(0)
; main.hpp
#include <ostream>
#include <istream>
#include <cstring>
#include <cassert>using std::ostream;
using std::istream;class my_string
{
private:char *data;int len = 0;private:bool copy_and_new(const char *src, char **dst, const int len);public:my_string();my_string(const char *ptr);my_string(const my_string &);my_string(const my_string &&);~my_string();public:my_string& operator=(const my_string&);my_string& operator+=(const my_string&);bool operator==(const my_string &rst);char operator[](unsigned int id);const char operator[](unsigned int id) const;public:friend my_string operator+(const my_string& rst, const my_string &snd);friend ostream& operator<<(ostream& os, const my_string&);friend istream& operator>>(istream& os, const my_string&);public:size_t size() const{return len;}
};my_string operator+(const my_string& rst, const my_string &snd);
ostream& operator<<(ostream& os, const my_string&);
istream& operator>>(istream& os, const my_string&);void string_test();
main.cpp
#include "my_string.hpp"
#include "ios_lib.h"my_string::my_string()
{data = new char[1];*data = '