C++感悟与积累
C++感悟与积累
各种初始化问题
默认初始化
指定义变量时没有指定初值时进行的初始化操作。这些变量被定义了而不是仅仅被声明(因为没有extern
关键字修饰),而且没有显式的赋予初值。
特别的,如果采用动态分配内存的方式(即采用new
关键字)创建的变量,不加括号时也是默认初始化,加了括号为值初始化。
如:
1
2
3
4
5
6 // 默认初始化
int a;
Sales_data myData;
int *p=new int;
// 值初始化
int *p=new int();
变量的值与变量的类型与定义的位置有关系:
- 对于内置类型变量(如int,double,bool等),如果定义在语句块外(即{}外),则变量被默认初始化为0;如果定义在语句块内(即{}内),变量将拥有未定义的值
- 对于类类型的变量(如string或其他自定义类型),不管定义于何处,都会执行默认构造函数。如果该类没有默认构造函数,则会引发错误。因此,建议为每个类都定义一个默认构造函数(=default)。
值初始化
值初始化是值使用了初始化器(即使用了圆括号()
或花括号{}
)但却没有提供初始值的情况。
注意,当不采用动态分配内存的方式(即不采用new
运算符)时,写成int a();
是错误的值初始化方式,因为这种方式声明了一个函数而不是进行值初始化。如果一定要进行值初始化,必须结合拷贝初始化使用,即写成int a=int();
值初始化和默认初始化一样,对于内置类型初始化为0,对于类类型则调用其默认构造函数,如果没有默认构造函数,则不能进行初始化。
如:
1
2
3 // 值初始化
int *p=new int();
vector<string> vec(10);
直接初始化与拷贝初始化
直接初始化与拷贝初始化对应,其内部实现机理不同。直接初始化是指采用小括号的方式进行变量初始化(小括号里一定要有初始值,如果没提供初始值,那就是值初始化了!)。
拷贝初始化是指采用等号=
进行初始化的方式。拷贝初始化看起来像是给变量赋值,实际上是执行了初始化操作,与先定义再赋值本质不同。
区别:
- 对于内置类型变量(如
int
,double
,bool
等),直接初始化与拷贝初始化差别可以忽略不计。 - 对于类类型的变量(如string或其他自定义类型),直接初始化调用类的构造函数(调用参数类型最佳匹配的那个),拷贝初始化调用类的拷贝构造函数。
特别的,当对类类型变量进行初始化时,如果类的构造函数采用了explicit
修饰而且需要隐式类型转换时,则只能通过直接初始化而不能通过拷贝初始化进行操作。
如:
1
2
3
4
5
6
7
8 // 直接初始化
int a(12);
Sales_data myData(para);
vector<int> ivec(ivec2);
string s("123456");
// 拷贝初始化
int a=12;
string s = string("123456");
列表初始化
列表初始化是C++ 11 新引进的初始化方式,它采用一对花括号(即{}
)进行初始化操作。能用直接初始化和拷贝初始化的地方都能用列表初始化,而且列表初始化能对容器进行方便的初始化,因此在新的C++标准中,推荐使用列表初始化的方式进行初始化。
列表初始化使用的是花括号而不是圆括号!
列表初始化若失败,则编译器会采用直接初始化的方式(将花括号{}
换成圆括号()
进行尝试)进行初始化
如:
1
2
3
4
5
6
7 int a{12};
string s{"123"};
vector<int> vec{1,2,3};
vector<int> vec = {1,2,3};
// 直接初始化
vector<string> v1{10};// 效果同 vector<string> v1(10);
vector<string> v2{10, "hi"};// 效果同 vector<string> v1(10, "hi");
C中函数指针问题
假设func是一个函数的函数名,会有 func 、&func、*func 三者得到的值一样(对二维数组名做以上操作,得到的值也一样)
func
得到函数地址,是因为它是函数指示符。只有在作为sizeof
或者单目&
操作符的操作数时,它的类型才是函数;其它情况都会被转化为指向该函数的指针。&func
得到函数地址,是因为单目&
操作符本来就是用来取操作数的地址的。而根据上一条,此 处操作数的类型就是func 函数,所以这个表达式可以得到func函数的地址。*func
得到函数地址,是因为本来就有相关的规定,表达式*函数
的值是对应的函数指示符,于是参见第一条。
C++中的.*
运算符
这个操作符是两个操作符组成的,一个是点.
一个是星*
这两个操作符在C++中都有自己的作用。主要用处一般.
是成员调用,*
为取地址处的值。而.*
在一起连续使用的情况有很多。比如:
1 | class ob |
但是.*
两个操作符连在一起被称为一个操作符的情况只有一种,那就是在成员函数指针的调用上。
成员函数指针与普通函数指针有很大差别,所以C++为成员函数指针制定了一系列操作符.*
就是其中一个
1 | class ob |
成员函数运算符重载与非成员函数运算符重载
该处参考此篇博文
运算符重载一般具有以下原则:
- 不可引入新的运算符,如重载
**
来表示平方等。除了.
、.*
、::
、?:
四个运算符外其他运算符均可被重载 - 重载后的运算符与原来的运算符优先级、结合性以及操作数个数相同,如双目运算符不能重载为单目;
- 保留运算符本身的含义,如“+”号重载后应该保持其“求和”的自然含义;
- 操作数中至少有一个为
class
类型,如重载运算符中参数都是int、double等类型是不允许的;
对于运算符重载可通过成员函数和非成员函数实现,这二者的区别如下:
- 成员函数运算符重载时,运算符的左值为调用对象,右值为参数对象;而在非成员函数运算符重载中,必须将操作数全部显式添加在参数列表中,运算符左值为第一个参数,运算符右值为第二个参数。(非成员函数在进行运算符重载时,由于需要访问类中的成员,应当将该非成员函数声明为友元函数。因此,非成员函数运算符重载一般都是友元函数。)
例子:
a,b均是类A的对象,重载“+”实现a+b,可以将其认为是:a对象调用“+”函数,函数的参数为b对象;而在事实上,a对象和b对象都是这个“+”函数的参数,只不过a对象被隐式调用,由this指针所绑定。因此成员函数运算符重载的显式参数比实际运算参数少一个
- 成员函数运算符重载时,运算符左值类型必须为所在类类型;而非成员函数运算符重载则不必。
一般来说,对于双目运算符,应当将其重载为非成员函数(友元函数),而对于单目运算符,则应将其重载为成员函数。
但这也不是绝对的,双目运算符中,=
、[]
、->
和()
是必须重载为成员函数的。而<<
运算符由于其第一个运算符必须是ostream对象,所以只能重载为非成员函数。
=
、[]
、->
和()
运算符之所以必须作为成员函数进行重载,其原因是因为:在成员函数重载时,会自动将this指针绑定到左值上,这样也就强制规定了运算符左值的类型,如果不这样,而是通过非成员函数进行重载,那么很有可能会出现类似“3=a”、“3[a]”、“3->a”以及“3(a)”的情况,单从运算符重载函数上说,这些似乎都是对的,但是实际上这些在语法上都是错误的,是需要坚决避免的,为了避免出现这些情况,就应当将这四种运算符重载限定在成员函数中
强制类型转化
四种强制类型转化:
- dynamic_cast 主要用于执行“安全的向下转型(safe downcasting)”
- static_cast 可以被用于强制隐形转换,它还可以用于很多这样的转换的反向转换
- const_cast一般用于强制消除对象的常量性
- reinterpret_cast 是特意用于底层的强制转型,导致不可移植的结果。主要用于进行各种不同类型的指针之间、不同类型的引用之间以及指针和能容纳指针的整数类型之间的转换。转换时,执行的是逐个比特复制的操作。
static_cast和reinterpret_cast
相同点:都是暴力转换,从一个类型转换为另一个类型,对于类指针不会保证安全性
区别:主要在于多重继承
例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 class A {
public:
int m_a;
};
class B {
public:
int m_b;
};
class C : public A, public B {};
C c;
printf("%p, %p, %p", &c, reinterpret_cast<B*>(&c), static_cast <B*>(&c));则有:
前两个的输出值是相同的,最后一个则会在原基础上偏移4个字节,这是因为static_cast计算了父子类指针转换的偏移量,并将之转换到正确的地址(c里面有m_a、m_b,转换为B*指针后指到m_b处),而reinterpret_cast却不会做这一层转换
auto与decltype
auto
早在C++98标准中就存在了auto关键字,那时的auto用于声明变量为自动变量,自动变量意为拥有自动的生命期,这是多余的,因为就算不使用auto声明,变量依旧拥有自动的生命期
在C++11标准中,auto可以在声明变量的时候,编译器根据变量初始值的类型自动为此变量选择匹配的类型
注意:
-
auto 变量必须在定义时初始化(auto是编译器通过初始值推断出来的)
-
在一条语句中定义多个变量时,auto始终只能推导成同一类型
-
引用是其所指对象的同义词,只用在
decltype
中可以推断出,auto中不能 -
auto会忽略顶层const或volatile,而底层const会保留
1
2
3const int ci = 0;
auto b = ci;//int
auto c = &ci;//const int *-
可以通过指定const来保留顶层const
1
const auto d = ci;//const int
-
如果类型为auto的引用时,顶层const仍保留
1
auto &f = ci;//const int &
-
-
初始化表达式为数组时,auto关键字推导类型为指针,
decltype
为数组类型1
2
3int a3[3] = { 1, 2, 3 };
auto b3 = a3;//int *
decltype(a3) a4 = {4, 5, 6};//int [3]-
如果类型为auto的引用时,则推导类型为数组类型
1
auto &b3 = a3;//int [3]
-
-
函数或者模板参数不能被声明为auto
-
auto仅仅是一个占位符,它并不是一个真正的类型,不能使用一些以类型为操作数的操作符,如
sizeof
或者typeid
1
2cout << sizeof(auto) << endl;//错误
cout << typeid(auto).name() << endl;//错误
decltype
decltype关键字是选择并放回操作数的数据类型,在此过程中,编译器分析表达式并得到他的类型,到那时不计算表达式的值
注意:
-
如果表达式为变量,则返回变量的类型(包括顶层const和引用)
1
2
3const int ci = 0, &cj = ci;
decltype(ci) x = 0;//const int
decltype(cj) y = x;//const int & -
如果表达式求值结果为左值,则返回一个引用类型
1
2
3
4
5int *p = NULL, i = 42;
// 解引用运算符生成左值
decltype(*p) a = i;//int &
// 取地址运算发生成右值
decltype(&p) b = NULL;//int ** -
decltype的结果类型与表达式密切相关:
1
2
3
4
5int i = 23;
//可以理解为赋值语句左值的特殊表达式
decltype((i)) c;//int &
//变量
decltype(i) d;//intdecltype(())
形式永远是引用
-
函数相关:
1
2
3decltype(f()) sum = x;//函数f放回值类型
decltype(f);//函数f类型
decltype(f)*;//函数f指针类型
输入
C语言输入处理:
scanf
:返回值为正确捕获的变量数,输入错误时返回EOF
,可以用while(scanf() != EOF)
来处理所有输入(默认以空格、tab、回车做分隔符)getchar
:获得一个字符putchar
:输出一个字符gets
:输入字符串时包括所有,遇到回车结束,将回车替换为\0
atoi
:const char*
转int
itoa
:int
转const char*
例子:
1
2
3
4
5
6
7 //以','和'\n'为分隔符
while(scanf("%[^,\n]", s[i]) != EOF)
{
...
//scanf会将分隔符保留着缓存区中,getchar读取这个缓冲区
if(getchar() == '\n'){}
}
getline
string中的getline不是string的成员函数,属于全局函数,使用需要#include<string>
,有两个重载版本:
1 | istream& getline(istream& is, string& str, char delim);//指定分界符 |
读取流里的字符串到str中,直到遇到下列情况结束:
- 遇到文件结束符,如windows下模拟的
ctrl+z
,或无效输入 - 遇到换行符,即Enter,将换行符留在缓冲区中,并不存入str中
- 遇到设备故障
注意:
- 如果第一个字符为换行符,getline将结束,str被置为空串。
- 注意连续调用getline时换行的问题
- getline函数返回istream引用的对象,可以用作condition
cin.getline
获取一行的字符,直到读取到前n个字符或者遇到分隔符则停止,会舍弃分隔符
1 | istream& getline(char* s, streamsize n); |
- 第一个参数为字符指针
- 第二个控制输入的最大字符(实际有效字符为n-1,最后一个位置存’\0’,以便把输入存为字符串),如果输入的字符串过长,会导致cin流状态无效,不能再输入
- 使用
cin.clear()
函数可以重设cin的状态为有效,cin.sync()
清空清空缓存区 - 该函数读取到分界符或最大数目的字符后即停止
- 当没有遇到分界符时,字符串长度超过n-1即为过长,cin会无效
- 使用
- 第三个为指定的分隔符,默认为
'\n'
注意:
- 会将分界符丢弃,即缓冲区不会有分界符用于下一个输入
- 如果输入的字符串过长,余下的字符会留在缓冲区
cin.get
1 | istream& get(char*,int,char); |
遇到换行符或分界符,get会保留该字符在缓冲区中(除了后面两个用于单字符的)
例子
以‘,’为分隔符
1 | #输入 |
字符串:
1 |
|
stringstream:
1 |
|
以空格为间隔
直接cin即可
多组测试数据
1 | # 输入的N组数据 |
cin套个while即可
1 | int n; |
多行多项数据
1 | # 输入 |
套用stringstream即可:
1 | int m; |
内存对齐
参考资料:C++内存对齐总结
#pragma pack (n)
:指定对齐值n。如果n大于此结构体中最大基本数据类型的大小,那么依据最大基本数据类型大小对齐;否则,依据n进行对齐;即:按照最小值进行对齐__attribute__((align(n))
: 如果n大于此结构体中最大基本数据类型size,那么依据最大基本数据类型size对齐;否则,依据n进行对齐
规则:
- 第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照
#pragma pack
指定的数值和这个数据成员自身长度中,比较小的那个进行。 - 在数据成员完成各自对齐之后,类(结构或联合)本身也要进行对齐,对齐将按照
#pragma pack
指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。
注意:
#pragma pack
后的n可以缺省,缺省数值是按照编译器自身设置,一般为8,合法的数值分别是1、2、4、8、16,只能为2的倍数- 一般编译器中,在32位系统和64位系统中,只有以下两种标准类型有区别,其余类型长度均不变:
- long:32位系统为4字节,64为系统为8字节
- 指针:32位系统为4字节,64为系统为8字节
例子:
1 | class test { |
对象的内存分配
参考文献:
C++中对象模型在不同的编译器下以及不同的C++版本均有所不同,这里说的是其中的一种。其虚函数表放在对象最开始的位置。
对于如下类(不含成员变量):
其虚函数表的基本结构如下所示:
对于如下类(包含成员变量):
其虚函数表的基本结构如下所示:
- 注意:上述Base1下的下划线只表示分界线,并非base1的起始地址,base1的第一个元素是虚函数表的地址
总结如下:
- 子类的虚函数会覆盖每个父类中对应的虚函数
- 子类函数会放在第一个虚函数表之后
- 如果每个父类均有自己的数据,则在内存中是按照先放虚函数表再放数据的规则进行排列
- 如果有虚类,会放在实例化对象内存空间的最后
大小端
- 大端模式: 数据的高字节存在低地址,数据的低字节存在高地址
- KEIL C51(89C52)
- 网络上传输数据
- Java与平台无关,但默认是大端
- 小端模式: 数据的高字节存在高地址,数据的低字节存在低地址
- X86结构
- 很多的ARM(部分,如stm32,可以随时在程序中(在ARM Cortex 系列使用REV、REV16、REVSH指令)进行大小端的切换),DSP
区分例子:
1 | void SysCheck() |
例子2:
1 | void find_cpu_endian(void) |
宏:
1 | #definesw16(x)\ |
调试分析工具
静态分析
cppcheck
主要用于对C/C++源代码进行分析检查的一个开源工具,可以用来检测未使用的变量、越界访问、内存泄漏等问题
使用方法:
1 | cppcheck --enable=all NAME.cpp |
gcov
用来检查代码中各个语句的执行次数,查看代码执行逻辑,方便后期对代码的优化。
使用方法:
- 在gcc/g++的编译选项中添加两个选项
-fprofile-arcs
、-ftest-coverage
- 运行可执行程序,生成两个包含代码覆盖信息的两个文件 .gcno .gcda
- 执行命令
gcov NAME.cpp
会生成包含代码执行次数的信息的文件NAME.cpp.gcov;通过该文件可以查看每行代码的调用次数
ldd
可以查看当前可执行程序(或者动态库)需要依赖哪些动态库,以及缺少哪些动态库。ldd -r
还可以报告缺少的目标对象和函数
使用方法:
1 | ldd FILENAME |
注意:
对于引用第三方动态库的程序在运行的时候提示找不到对应的动态库,通常是因为动态库并未在
ld.config
文件中写明的路径,且在链接的过程中使用了-L PATH_DIR -lNAME
这样显示指明动态库位置的选项。可以通过
readelf -d FILENAME
查看调用动态库的位置。具体解决方式为:
- 链接的时候配合使用
-Wl,-rpath=PATH_DIR
和-L PATH_DIR
两个参数选项,保证程序在链接、运行期间能够正确找到动态库的位置(-Wl,-rpath
是在运行时起作用,-L
是在链接时起作用;另外,如果PATH_DIR是相对路径,在使用-rpath
的时候需要使用ORIGIN这个宏,例如-Wl,-rpath='$$ORIGIN/lib'
这个选项就是说,运行的时候在可执行文件所在目录的lib子目录中寻找动态库)- 将动态库所在的目录添加到
/etc/ld.config
文件中,然后执行ldconfig
刷新缓存(不推荐使用)
file
查看文件的类型,对于可执行文件或者动态库,可以查看是否需要链接动态库,同时也可以查看是否包含符号表(调试用,可以通过strip去除)
使用方法:
1 | file FILENAME |
readelf
用来查看头信息,符号信息,动态重定位信息等elf内部的各个部分
使用方法:
1 | readelf -h FILENAME |
- -h:显示头信息
- -s:显示符号信息(注:如果是c++的话,函数名会被编码成类似_ZN4hoot3Log11getInstance形式。这种情况下,建议采用
nm -CD
命令选项来进行函数的查找)
nm
列出目标文件中的符号
使用方法:
1 | nm -CDu FILENAME |
-C
:显示可读的符号形式-D
:显示动态符号-u
:显示未定义的symbol(通常在其他文件中定义)
strip
用来清理共享库或可执行文件中的符号信息和调试信息,通常是程序正式发布前进行
调试跟踪
gdb
可以调试未运行的程序或者正在运行的程序,还可以分析程序崩溃的coredump文件,这些的前提是,程序在编译时添加了-g
选项打开了调试信息。
使用方法:
1 | gdb FILENAME #对于有参数的,可以通过gdb内执行 set args PARAM 来设置参数 |
调试时,常用命令:
bt(backtrace)
、where
:查看函数的调用栈l(list)
:显示源代码,并且可以看到对应的行号i(info) registers
:列出寄存器的信息(不包括浮点寄存器与向量寄存器)i(info) all-registers
:列出所有寄存器的信息(包括浮点寄存器与向量寄存器)i(info) registers regname
:列出单独的寄存器set var $pc=0x08050949
:修改寄存器值
b(break)x
,x是行号:在对应的行号位置设置断点p(print)x
,x是变量名:打印变量x的值p $regname
:打印寄存器值
r(run)
:继续执行到断点的位置n(next)
:执行下一步(单步执行)c(continue)
:继续执行q(quit)
:退出gdb
coredump文件
在进程收到某些信号而终止运行时,将此时进程地址空间的内容以及有关进程状态的其他信息写到core文件中(如非法访问内存产生segment fault错误),利用gdb可以查看到底是哪里发生了异常
通常情况下,core文件会包含了程序运行时的内存,寄存器状态,堆栈指针,内存管理信息还有各种函数调用堆栈信息等,通过工具分析这个文件,可以定位到程序异常退出的时候对应的堆栈调用等信息,找出问题所在并进行及时解决
core文件默认的存储位置与对应的可执行程序在同一目录下,文件名是core,可以通过下面的命令看到core文件的具体位置:
1 | cat /proc/sys/kernel/core_pattern |
同时,一般登陆进入系统后,系统限制不生成core文件,需用通过以下命令生成:
1 | # 查看当前shell的限制 |
通过gdb对coredump文件定位问题:
1 | gdb program core |
- program:可执行程序名
- core:生成的 core 文件名
strace
跟踪并显示用户程序中的系统调用的详细信息(参数、返回值、系统调用耗费时间等)。适用于可执行程序或者运行中的进程,用户可以观察程序的运行状态
1 | strace FILENAME |
- -c:统计每次调用的时间、次数等信息
- -f:跟踪fork产生的子进程
- -tt:输出的每行内容前添加时间信息
- -T:显示每次调用耗费的时间
- -e:后接相关的系统调用方法,只显示特定类型的调用信息
pstack
查看进程的实时堆栈信息
1 | pstack PID |
valgrind
用于程序内存泄漏检查,同时它还有程序性能分析的功能(用得少)
1 | valgrind --leck-check=full FILENAME |
性能分析
perf
用来分析应用程序或者内核代码性能。可以对单个程序做函数调用次数、上下文切换次数、中断次数等信息进行统计
使用方法:
1 | # ubuntu 上需要先安装以下软件 |
gprof
与perf
功能类似,也是主要用户程序性能分析。使用gprof
要求在编译链接的时候,添加-pg
选项,然后执行程序,会生成包含性能统计信息的gmon.out
文件(默认在程序结束时生成),然后再使用gprof
分析这个gmon.out
文件来读取程序相关的性能信息
如果想分析一个长期运行的程序,需要加入信号处理函数来让程序调用
exit
主动退出而不是Ctrl C
强制退出(强制退出不会产生统计信息)
使用方法:
1 | gprof FILENAME gmon.out |