目录
我们承担ROS,FastDDS等通信中间件,C++,cmake等技术的项目开发和专业指导和培训,有10年+相关工作经验,质量有保证,如有需要请私信联系。
POD类型
平凡的和标准布局的——貌似和深度探索C++对象模型中关于按位拷贝冲突
- 平凡的定义:符合以下四点定义:是不是可以总结为:构造,析构,拷贝/移动构造/赋值运算符 都是自动生成,不包含虚函数/虚基类
- 有平凡的构造和析构:即构造和析构什么都不干,而一旦定义了构造或析构,即使不含参数,实现为空,也不再平凡,需定义成default;一般默认生成即可
- 平凡的拷贝构造和移动构造:平凡的拷贝构造等同于使用memcpy进行类型的构造;默认生成或显式定义成default
- 平凡的拷贝赋值运算符和移动赋值运算符,这基本上同平凡的拷贝构造和平凡的移动构造类似
- 不能包含虚函数和虚基类
- 标准布局:
- 所有非静态成员都有相同的访问权限
- 类或结构体继承时,满足以下两种情况之一:就是非静态成员不能同时出现在基类和派生类中
- 派生类中有非静态成员,且只有一个仅包含静态成员的基类
- 基类有非静态成员,而派生类没有非静态成员
- 类中第一个非静态成员的类型与其基类不同
- 没有虚函数和虚基类
- 所有非静态数据成员均符合标准布局类型,其基类也符合标准布局
- pod类型相关的类模板工具
#include<type_traits>
:template<typename T>struct std::is_trivial
的成员value可以用于判断T类型是否是一个POD类型;除了类和结构体之外,is_trivial可以对内置的标量类型数据及数组类型进行判断template<typename T>struct std::is_standard_layout
的value可以判断类型是否是一个标准布局的类型template<typename T>std::is_pod<T>::value
可以判断一个类型是否是POD,T可以是基本类型
- 使用POD类型有什么好处:
- 提供对C内存布局兼容,C++程序和C函数进行相互操作,因为POD类型的数据在C与C++间的操作总是安全的
- 字节赋值:可以安全的使用memset和memcpy对POD类型进行初始化和拷贝等操作
- 保证了静态初始化的安全有效。静态初始化在很多时候能够提高程序的性能,而POD类型的对象初始化往往更简单—(比如放入目标文件的bss段,在初始化中直接赋值为0)。
左值和右值
- 左值:可以取地址,有名字的就是左值,反之就是右值(更简单的,=左边的就是左值,右边的就是右值),调用一个返回引用的函数得到的是左值,其他返回类型得到右值,特别的,可以为返回类型是非常量引用的函数的结果赋值
- 右值:C++11中的右值分为纯右值和将亡值
- 纯右值:之前的右值概念
- 将亡值:C++11中新增的跟右值引用相关的表达式,如返回右值引用T&&的函数返回值,或者转换为T&&的类型转换函数的返回值,std::move的返回值
- 右值引用:对一个右值进行引用的类型;右值引用也必须立即初始化;右值引用是为了支持移动操作而引入的
- 一般右值不具有名字,通常情况下只能从右值表达式获取其引用,如
T&& t = ReturnRValue();
函数返回的右值在表达式语句结束后生命就结束了,通过右值引用的声明,该右值又重获新生,其声明周期和右值引用类型变量t的生命周期一样,不过需要注意,能够声明右值引用t的前提是ReturnRvalue返回的是一个右值 - 左值引用和右值引用都是都必须立即初始化,左值引用是具名变量值的引用,右值引用是不具名变量值的引用
- 右值引用不能绑定到任何左值,只能绑定到一个将要销毁的对象,如
int c; int &&d=c;
是不能通过编译的 - 左值引用是否可以绑定到右值?
- 右值引用的来由从来就跟移动语义紧密相关,这是右值存在的最大价值之一
- 为什么不使用常量右值引用?
- 一. 右值引用主要是为了移动语义,而移动语义需要右值是可以被修改的;
- 二,如果要引用右值且不可以更改,常量左值就足够了
- 一般右值不具有名字,通常情况下只能从右值表达式获取其引用,如
T& e = ReturnRvalue(); // 编译错误
const T& f = ReturnRvalue(); // 编译成功,常量左值引用在C++98中是个万能的引用类型,可以接受非常量左值,常量左值,右值对其进行初始化,而且在使用右值对其进行初始化的时
// 候常量左值引用还可以像右值引用一样将右值的生命期延长。
所以说,在定义一个移动构造函数的时候,定义一个常量左值引用的拷贝构造函数,是一种非常安全的设计——移动不成还可以执行拷贝
const bool & judge = true;
const bool judge2 = true;
// 上面两条的区别就是:前者直接使用右值并为其续命,后者的右值在表达式结束后就销毁了
- 判断是否是引用类型/左值引用/右值引用,通过类模板的成员value获取,详细知识点参考这里
is_rvalue_reference
is_lvalue_reference
is_reference
static
static关键字用于内部链接(静态链接)
- 全局static变量:
- 全局static变量的声明与定义同时进行,即当你在头文件中使用static声明了全局变量,同时它也被定义了,如果没有明确的值会赋值为0(NULL, “”),不像其他变量不明确
- static修饰的全局变量的作用域只能是本身的编译单元。在其他编译单元使用它时,只是简单的把其值复制给了其他编译单元,其他编译单元会另外开个内存保存它,在其他编译单元对它的修改并不影响本身在定义时的值。即在其他编译单元A使用它时,它所在的物理地址,和其他编译单元B使用它时,它所在的物理地址不一样,A和B对它所做的修改都不能传递给对方。这也是不能用extern修饰static变量的原因,extern修饰全局变量和函数,在其他文件中是通用的;这也是多个地方引用静态全局变量所在的头文件不会出现重定义错误的原因,因为在每个编译单元都对它开辟了额外的空间进行存储。
- C++中,全局static变量和class的static成员在main之前先初始化,main之后销毁。但是,C++并没有规定,不同源文件中的非局部变量(static全局变量,class staitc数据成员,全局变量)的初始化顺序,也就是说,不同源文件中非局部变量的初始化顺序是不确定的,最好不要有一个依赖另一个的情况。同样,销毁顺序也是不确定的。
- 函数内的static变量在什么时候执行初始化?
- 如果初始值是一个编译期常量,在函数的任何一行执行之前就已经初始化了
- 如果初始值不是一个编译期常量,或者静态变量是一个拥有构造函数的对象,在执行期初始化,也就是函数第一次调用时初始化
- 类内static成员变量:
- 定义:static关键字只出现在类内部的声明中,类外定义时不能重复static变量,需要指定对象的类型和作用域
- 初始化:不能在类内部初始化静态成员,相反必须在类的外部定义和初始化每个静态成员;但是从C++17开始,可将静态数据成员声明为inline,这样就不需要再类外定义。如:
static inline int a{0};
- 生命周期:类似于全局变量,静态数据成员定义于任何函数之外,因此一旦被定义,就存在于程序的整个生命周期
- 访问:类外可以使用实例访问或类名加作用域运算符访问
- static数据成员是常量,可以定义为const static或者static const,可以在类内定义和初始化基本数据类型而不需要将其指定为内联。
- static变量只会被初始化一次,下次进来的时候不会重新执行,以此特性可以使用单例模式
- static函数
- 同static变量一样,static函数是静态链接,只在本编译单元内有效。在其他编译单元访问会报函数未定义的错误。
- 要获得静态链接这个特性时现在推荐使用匿名的命名空间,而不要使用static关键字,但有一点不同:
- static不管定义和声明是否都在.h中,其他编译单元中都可以使用
- 匿名命令空间中的函数如果定义在cpp文件中无法使用,如果在头文件中可以在其他编译单元中调用。
- static成员函数:static成员函数是否可以是const?static成员函数是cons没有任何实际意义。因为cons是修饰this指针的,static是没有this指针的。const成员函数是用来保证不会修改对象的状态,而static成员函数是不与对象相关的,它无法访问非静态成员变量和非静态成员函数,所以声明它为const基本上没有任何意义。
如果你尝试在C++中将static成员函数声明为const,编译器会给出错误信息
extern
extern关键字好像是static的反义词,将他后面的名称指定为外部链接。在某些情况下可以使用这种方法,如,默认情况下,const和typedef具有内部链接,可以使用extern使其变为外部链接。
当指定某个名称为extern时编译器会将这条语句当作声明,而不是定义。对于变量这意味着编译器不会为其分配空间,必须为这个变量提供单独的,而不是使用extern关键字的定义行。例如:
extern int x;
int x = 3;
或者,可以在extern语句中初始化x,让后将其作为声明和定义
extern int x = 3;
但在这种情况下,x不是特别有用,因为x本身具有外部链接
const
const变量
- const对象必须初始化,而且是在声明的时候初始化。其对象一旦创建后就不能再改变,在const类型的对象上只能执行不改变其内容的操作
- 声明时加extern,再定义(定义时可加可不加),这时候包含声明const头文件的多个文件共享同一个const对象
- const对象仅在文件内有效。如果声明时不加extern直接定义,包含了const变量头文件的编译单元会开辟新的内存存放该变量,类似static变量,不会报链接时重定义,但地址不同
- 初始化对const的引用:允许为一个常量引用绑定非常量的对象,字面值(右值),甚至一般表达式,但数据类型是要一致的;常量引用仅对引用可参与的操作做出了限定,对引用的对象本身是不是常量未做限定,所以允许通过其他途径改变它的值;
const int &r = 42;
常量值(右值)作为实参传递给const T&
也是允许的 - 初始化指向常量的指针:同初始化对const的引用,允许一个指向常量的指针指向一个非常量对象;指向常量的指针要求不能通过该指针改变对象的值,但没有规定那个对象的值不能通过其他方式改变;
- 如果用一个对象去初始化另一个对象,则它们是不是const(顶层const)都无关紧要
int i = 42;
const int v = i; // 可以用非const变量初始化const变量
int j = v; // 可以用const变量初始化非const变量
但注意拷入和拷出的对象必须具有相同的底层const,将底层const类型给非const类型是不允许的
int *const a = new int{2};
int *b = a; // ok,将顶层const指针给非const指针
int *const c = b; // ok,将非const指针给顶层const指针
int const* d = new int{3};
int* e = d; // error,将底层const指针给非const指针
int const* f = b; // ok,将非const指针给底层const指针
int g = 4;
const int& h= g; // ok,将非const引用给const引用
int & i = h; // error,将const引用给非const引用
- 两个概念
- 顶层const:const作用于对象本身,即本身是一个常量,常量指针(*const)是顶层const
- 底层const:所指的对象是一个常量;如指针常量(const* )和声明引用的const都是底层const
const成员函数
- 默认情况下,this指针是类类型非常量版本的常量指针(如class A,默认this类型是
A *const
;加上const后的类型为为const A *const
),这一情况使得不能在一个常量对象上调用普通的成员函数 - 在设计类的时候,一个原则就是对于不改变数据成员的成员函数定义为const函数,在类中声明方法时可以不带const,在类外定义时要加上const。
- 基于const的重载:可以被具有相同形参列表的const和非const成员函数重载,在这种情况下,类对象的常量属性决定了调用哪个方法
- const对象能调用const成员函数但不能调用非const成员函数,非const对象是可以调用const成员函数的
C++中的关键字
mutable
:可变数据成员,在const成员函数内也能改变数据成员,通过在变量的声明中加入mutable。一个mutable数据成员永远不会是const,即使是const对象的成员__declspec (novtable )
:表示这个类不生成虚函数表,但继承类不受影响;使用此关键字相对节省空间——待验证delctype
:类型指示符,作用是选择并返回操作数的数据类型register
:让编译器将变量直接放入寄存器中,以提高存取速度——待验证volatile
:阻止编译器进行优化- 用法和const类似,起到对类型额外修饰的作用,只有volatile的成员函数才能被volatile的对象调用
- 只能将volatile对象的地址赋给一个指向volatile的指针,同时只有当某个引用是volatile的时候才能使用一个volatile对象初始化该引用
- const和volatile的一个重要区别是不能使用合成的拷贝/移动构造函数及赋值运算符初始化volatile对象或从volatile对象赋值。合成的成员接收到参数类型是非volatile,不能把一个非volatile引用绑定到一个volatile对象身上。
- 如果一个类希望拷贝,移动或赋值它的volatile对象,则该类必须自定义拷贝或移动操作
class Foo
{
public:
Foo(const volatile Foo&);
Foo& operator=(volatile const Foo&);
Foo & operator=(volatile const Foo&) volatile;
};
union
一种节省空间的类
- 一个union可以有多个数据成员,但在任意时刻只能有一个数据成员可以有值,当给某个成员赋值之后,该union的其他成员就变成未定义状态了。
- 分配一个union的对象的存储空间至少要能容纳它的最大的数据成员
- 和其他类一样,一个union定义了一种新类型
- union不能含有引用类型的成员
- 在C++11新标准中,含有构造和析构的类类型成员也可以作为union的成员类型,union可以为其成员指定public/protected/private等标记,默认情况下union的成员都是公有的
- 匿名union:
嵌套类
类定义不仅可以包含成员函数和数据成员,还可以编写嵌套类、嵌套结构体、枚举类型、声明类型别名
- 如果声明内容为public的,可在类外使用
ClassName::
作用域运算符访问(ClassName为外围类类名);如果声明了一个private或protected嵌套类,这个类只能在外围类中使用 - 嵌套类可以访问外围类中的所有private或protected成员,而外围类只能访问嵌套类中的public成员
- 嵌套类定义在类内显得有些臃肿,可以在外围类中声明,然后独立地定义嵌套类。
class MyClass {
public:
class QianTao;
...
};
class MyClass::QianTao { ... };
基础知识点
- 栈是可以动态分配的,alloca函数可以动态分配栈空间,且不需要手动释放,由编译器生成的释放函数释放栈空间
- 全局变量和函数:在. h中声明时加上extern,在.cpp中定义(不需要加extern);包括了声明了extern头文件的文件中可以直接使用该变量,如果没有包含头文件,需要加上extern声明
- 不完全类型使用情景:只能是本类型的指针或引用,声明以不完全类型作为参数或返回值的函数;所以类允许包含指向自身的指针或引用
- 单一定义规则(ODR):可以多处声明但只能单一定义;在一个头文件中不要定义,因为C++的编译单位是cpp文件,如果这个头文件被多个cpp文件包含,在.o文件链接的时候会报多处定义的错;如果非要用,可以定义成static变量;或使用extern
- 引用和指针的区别:
1. 引用必须要初始化,且不能为空,指针可以为空——不存在指向空值的引用意味着使用引用的代码效率比使用指针的要高,因为在使用引用前不需要测试它的合法性;相反指针应该总是被测试,防止其为空
2. 引用类型的变量会占用内存空间,占用的内存空间的大小和指针类型的大小是相同的。
3. 引用自始至终只能绑定一个变量,指针可以指向不同的地址
4. sizeof 运算符应用于引用,给出它所引用的元素的大小,所以用sizoef直接计算一个引用变量的大小取到的不是引用本身的大小 - 移位操作:
- 左移相当于乘,左移一位乘以2
- 右移相当于除,右移一位相当于除以2,右移两位相当于除以4
- 对于右移大于或等于位宽的操作,或者右移负数的操作,结果依赖于编译器,不唯一,vs上为原数不变
- C++确保delete一个空指针是安全的(vs实测delete一个NULL指针没事)
- 数组指针和指针数组
- 数组指针(也称行指针):是个指针,指向数组
- 定义
int (*p)[n]
; - ()优先级高,首先说明p是一个指针,指向一个整型的一维数组,这个一维数组的长度是n,也可以说是p的步长。也就是说执行p+1时,p要跨过n个整型数据的长度。
- 如要将二维数组赋给一指针,应这样赋值:
*int a[3][4];
int (*p)[4]; //该语句是定义一个数组指针,指向含4个元素的一维数组。
p=a; //将该二维数组的首地址赋给p,也就是a[0]或&a[0][0]
p++; //该语句执行过后,也就是p=p+1;p跨过行a[0][]指向了行a[1][]
- 定义
- 指针数组:是个数组,里面包含的是指针
- 定义 int *p[n];
- []优先级高,先与p结合成为一个数组,再由int*说明这是一个整型指针数组,它有n个指针类型的数组元素。这里执行p+1时,则p指向下一个数组元素,这样赋值是错误的:p=a;因为p是个不可知的表示,只存在p[0]、p[1]、p[2]…p[n-1],而且它们分别是指针变量可以用来存放变量地址。但可以这样 p=a; 这里p表示指针数组第一个元素的值,a的首地址的值。
- 这两者的区别:数组指针似乎是C语言里专门用来指向二维数组的,它占有内存中一个指针的存储空间。指针数组是多个指针变量,以数组形式存在内存当中,占有多个指针的存储空间。注意:char类型的不管是指针还是数组,都要以"\0"结尾,不然不知道何时结束,导致出现奇怪的值,或者长度和申请的长度不一致
- 数组指针(也称行指针):是个指针,指向数组
- strlen和sizeof的区别:
- strlen计算长度不包含终止null字节的字符串长度,sizeof则计算了包括终止null字节的缓冲区长度
- strlen需要一次函数调用,而对于sizeof而言,因为缓冲区已用已知字符串进行初始化,其长度是固定的,所以sizeof是在编译时计算缓冲区长度
- 编译器会在一个字符串字面常量的末尾插入一个空字符作为终结符
const char *a = "hello";
cout << sizeof(a) << endl; // 输出6
cout << strlen(a) << endl; // 输出5
- 小型对象分配:为什么需要小型对象分配器:
- C++中的new/delete只是对C heap分配器的包装,C heap分配器并未特别对小块内存的分配进行优化,C分配器通常用来分配中大型对象(数百或数千个bytes),所以导致分配速度慢
- C++缺省分配器的通用性也造成小型对象空间分配的低效。缺省分配器管理一个记忆池,而这种管理通常需要耗费一些额外内存,如果分配区块大的话这些开销是微不足道的,但如果分配区块小的话开销所占的比例就非常大了
- 在C++中动态分配很重要,执行期多态和动态分配的联系最为紧密,高效的C++程序开发缺省分配器的低劣性能成为一种障碍
- 输入/输出运算符的重载:输入输出运算符必须是非成员函数,一般声明为友元
- 输出运算符<<:
- 第一个形参是non-const的ostream引用,向流写入内容,所以是non-const,无法复制ostream对象,所以是引用; 第二个形参是一个常量的引用,是要打印的类类型
- 为了与其他输出运算符保持一致,operator<<一般返回它的ostream形参
- 输入运算符>>:
- 第一个形参是运算符将要读取的流的引用,第二个形参是将要读到的非常量对象的引用
- 输出运算符<<:
- 一种for的用法:(范围for循环?)
for (int elem : coll1) {
cout << elem << " ";
}
- float在计算机中的存储方式:
- 转换函数:
- C中整数,浮点数,字符串之间的转换:
atoi
:字符串转整数
- C++中的转换:
std::stod
: 字符串转浮点型
- C中整数,浮点数,字符串之间的转换:
- 数据类型的自动转换规则:
- 若参与运算的数据类型不同,则先转成同一类型,然后进行运算——应该是先计算,再反转
- 转换按数据长度增加的方向进行,以保证精度不降低。
- 所有的浮点运算都是以双精度进行的,即使仅含float单精度运算的表达式
- char和short参与运算时必须先转成int
- 在赋值运算中赋值号两边的数据类型不同时需要把右边的类型转成左边的类型
- 位域
- 一个位域含有一定数量的二进制位,当一个程序需要向其他程序或者硬件设备传递二进制数据时,通常会用到位域
- 位域在内存中的布局与机器无关
- 位域的类型必须是整型或者枚举类型,因为带符号位域的行为是由具体实现决定的,所以在通常情况下使用无符号类型保存一个位域
- 位域的声明形式是在名字之后紧跟一个冒号和一个常量表达式,该表达式用于指定成员所占的二进制位数
- 取地址运算符不能用于位域,因此任何指针都无法指向类的位域
- 通常使用内置的位运算符操作超过1位的位域
- 如果一个类定义了位域成员,则它通常也会定义一组内联的成员函数以检验或设置位域的值:
class File
{
unsigned int mode: 2; // mode占2位
unsigned int modefied: 1;
unsigned int prot_owner: 3;
unsigned int prot_group: 3;
unsigned int prot_world: 3;
public:
enum modes { READ = 01, WRITE = 02, EXECUTE = 03 };
File &open(File::modes m) {
mode |= READ;
// TODO 其他处理
if (m & WRITE) {
return *this;
}
}
inline bool isRead() const { return mode & READ; }
inline void setWrite() { mode |= WRITE; }
};
int b:0
表示一个零宽度的位字段,这种写法的主要用途是作为位字段的填充或者对齐。这就意味着,如果你有一个位字段,并且你希望下一个位字段在新的整数开始,你可以使用0宽度的位字段。例如,以下的结构体定义了一个包含两个位字段的结构体,其中b是一个0宽度的位字段,用来保证c位于新的整数开始:
struct BitField
{
int a : 4;
int b : 0;
int c : 4;
};
// 在这个例子中,a将占用4位,b不会占用任何位,c将在新的整数开始,并占用4位。
- 口算二进制:https://blog.csdn.net/devnn/article/details/82597660
头文件
- 在C++中,标准库头文件的名称省略了.h后缀,如
<iostream>
,所有文件都在std命名空间和std的子命名空间中定义。 - C中的标准库头文件在C++中依然存在,但使用如下两个版本:
- 不使用 .h 后缀,改用前缀 .c,这是推荐的版本,这些头文件将所有内容放在std命名空间中
- 使用.h后缀,这是旧版本。
浮点型数字
- 浮点型数字的保存
使用数量级不同的浮点型数字可能会导致错误。此外,计算两个几乎不相同的浮点数的差时会导致精度丢失。几个特殊的浮点型数: - +/-infinity:表示正无穷和负无穷
- NaN:非数字的缩写,例如0除以0的结果,在数学上是未定义的
可以用std::isnan()判断一个给定的浮点数是否为数字,用std::isinf()判断是否为无穷。这两个函数都定义在<cmath>
中。
可以使用std::numeric_limits来获取这些特殊的浮点数,例如:std::numeric_limits<double>::infinity
(见C++20 P12)
点分十进制与整数之间的转换
点分十进制(如IP地址)是一个非常常见的表示方式,它将一个32位的二进制数表示为4个十进制数,每个十进制数表示8位(一个字节)。如果你想将一个点分十进制的IP地址转换为一个整数,你可以使用以下步骤:
将点分十进制的IP地址分成4个部分:a.b.c.d
对每个部分进行转换:将a转换为二进制数,然后左移24位,得到a’;将b转换为二进制数,然后左移16位,得到b’;将c转换为二进制数,然后左移8位,得到c’;将d转换为二进制数,得到d’。
将四个部分相加:a’+b’+c’+d’,得到的结果就是IP地址对应的整数。
这样,你就可以将一个点分十进制的IP地址转换为一个整数了。例如,IP地址192.168.1.1转换为整数就是:192<<24 + 168<<16 + 1<<8 + 1 = 3232235777。
可以使用python计算:
ip=(a<<24)+(b<<16)+(c<<8)+d
编译与函数参数入栈总结
摘自:http://blog.csdn.net/frankiewang008/article/details/7481865
用法/技巧归纳
- 结构体初始化时变量名前加".":这是指定给哪个变量赋值,可以不用按顺序给结构体成员赋值
- main函数中可以省略return语句,这种情况自动返回0
- 一些备用参数或变量目前用不到会报警告,可以使用如下方法规避:
#ifndef UNUSED_ARG
#define UNUSED_ARG(arg_) (void)(arg_)
#endif /* UNUSED_ARG */
UNUSED_ARG(size);
- rapidjson中的document.h中有TypeHelper的类型萃取
- 判断参数为空的宏定义:RMW_CHECK_ARGUMENT_FOR_NULL
术语归纳
自由函数(free function):C++中的非成员函数都是free function