cmake教程(一)基本使用规则

查看cmake版本: cmake --version,Ubuntu自带的是3.5.1

cmake和qmake都用于产生Makefile,然后执行make命令进行编译,make还有其他参数,叫做make目标

升级 CMake

1
2
3
4
5
6
7
8
9
sudo apt-get install -y build-essential libssl-dev
wget https://github.com/Kitware/CMake/releases/download/v3.19.2/cmake-3.19.2.tar.gz
tar -zxvf cmake-3.19.2.tar.gz
cd cmake-3.19.2/
./bootstrap
make
sudo make install
hash -r
cmake --version

报错: Could not find CMAKE_ROOT !!! CMake has most likely not been installed correctly. 先执行hash -r再使用

还可参考: cmake源码安装 安装CMake和cmake-gui

cmake-gui

where is the source code里选择源代码位置,在where to build the binaries里选择编译出的文件,一般是在源代码目录里新建build文件夹。

先点一次Configure,出现配置对话框,选默认或者交叉编译(最后一个选项)。如果出现红色区域,再点一次。直到没有红色区域之后,点击configure,配置完成后点击generate,会在build文件夹下生成工程文件,然后去build文件夹里执行make

常见的make目标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
make all:编译程序、库、文档等(等同于make)

make install:安装已经编译好的程序,会默认把程序安装至 /usr/local/bin

make unistall:卸载已经安装的程序。

make clean:删除由make命令产生的文件(例如object file,*.o)

make -j N : 同时允许N个任务进行,如果不指定N则进行无限个任务,结果会将CPU性能榨干。N的最大值取决于CPU性能,不要把N设置太大,更不要不指定,否则可能死机。

make check:测试刚刚编译的软件(某些程序可能不支持,有时会测试很长时间,这就没有必要)

# 有时碰到这样的情况,第一次make会出错,再次make却能成功,以下命令用于这种情况
make -i: 忽略指令执行错误,并继续执行,且如果出错的话就会生成目标文件。这个一般用在调试的时候。
make -k: 出错也不停止运行,不仅可以忽略指令错误,而且还能忽略makefile规则错误

注释方法: CMake 3.0以上版本的多行注释:从#[[开始,在块注释的另一端以]]结束。但是并不好用,似乎和Tab对齐有关。



语法规则:

  • 变量使用${}方式取值, 但是在IF控制语句中是直接使用变量名
  • 环境变量使用$ENV{}方式取值, 使用SET(ENV{VAR} VALUE)赋值
  • 指令(参数1 参数2…) 参数使用括弧括起, 参数之间使用空格或分号分开

有时候看到一些工程有一个cmake文件夹,里面放几个cmake文件,打开发现是一些cmake的命令。这是因为工程规模太大,有些库比如PCL在每个CMakeLists里使用,每次都添加太麻烦,所以做成文件形式,在用到的时候在CMakeListsinclude(cmake/PCL.cmake)

基本规则

预定义变量,设置方式是set(VARIABLE value),大小写都是敏感的,可以用message函数查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
PROJECT_NAME   #通过PROJECT指令定义的项目名称
PROJECT_SOURCE_DIR #工程的根目录
PROJECT_BINARY_DIR #运行cmake命令的目录,通常是${PROJECT_SOURCE_DIR}/build
CMAKE_CURRENT_SOURCE_DIR #当前处理的CMakeLists.txt所在的路径
EXECUTABLE_OUTPUT_PATH #目标可执行文件的存放位置
LIBRARY_OUTPUT_PATH #目标链接库文件的存放位置

CMAKE_MAJOR_VERSION #cmake主版本号,如2.8.6中的2
CMAKE_MINOR_VERSION #cmake次版本号,如2.8.6中的8
CMAKE_PATCH_VERSION #cmake补丁等级,如2.8.6中的6
CMAKE_SYSTEM #系统名称,包含内核版本,例如Linux-2.6.22
CAMKE_SYSTEM_NAME #不包含版本的系统名,如Linux
UNIX #在所有的类UNIX平台为TRUE,包括OS X和cygwin
WIN32 #在所有的win32平台为TRUE,包括cygwin

BUILD_SHARED_LIBS #控制默认的库编译方式,默认编译生成的库都是静态库
CMAKE_C_COMPILER/CMAKE_CXX_COMPILER #指定C/C++编译器
CMAKE_CXX_FLAGS # 设置C++编译选项
CMAKE_BUILD_TYPE # 常用的编译类型 Debug Release
BUILD_SHARED_LIBS # 编译动态链接库(ON,OFF)

option函数

option(<variable> "<help_text>" [value])

  • variable 选项名
  • help_text 描述、解释、备注
  • value 选项初始化值(除ON而外全为OFF)

add_compile_options

下面的代码里,foo不会按照-Wall编译,但是bar会。

1
2
3
add_library(foo ...)
add_compile_options(-Wall 03)
add_library(bar ...)

-Wall显示所有警告。
O3(字母O)是编译优化的程度选择,有O1 O2 O3。数字越大编译优化越多,程序执行速度越快 但编译时间越长,不过一般程序看不出来。

CMAKE_CXX_STANDARD

CMake 3.1或者更高版本可使用set(CMAKE_CXX_STANDARD 11),不能用17

最好把add_compile_options(-std=c++17) 改为跨平台的写法: set(CMAKE_CXX_STANDARD 17),也就是x86arm平台

./configure命令

一般第三方库里都有个configure文件,它是个Shell脚本,内容很多../configure 是用来检测你的安装平台的目标特征的,比如它会检测你是不是有CC或GCC。主要功能是生成 Makefile,为下一步的编译做准备,你可以通过在./configure后加上参数来对安装进行控制,比如./configure --prefix=/usr是将该软件安装在 /usr 下面,执行文件就会安装在/usr/bin(而不是默认的 /usr/local/bin),资源文件就会安装在 /usr/share(而不是默认的/usr/local/share)

message函数

1
2
3
message(STATUS  "12345")   #如果不加STATUS,不会在前面加--标志
message(STATUS " include dirs: ${catkin_INCLUDE_DIRS}" ) #有一个--开头
message(STATUS " include dirs:" ${catkin_INCLUDE_DIRS} ) # 同上面等价

除了最常用的STATUS还有下面可使用的参数

  • (无) = 重要消息;
  • WARNING = CMake 警告, 会继续执行;
  • AUTHOR_WARNING = CMake 警告 (dev), 会继续执行;
  • SEND_ERROR = CMake 错误, 继续执行,但是会跳过生成的步骤;
  • FATAL_ERROR = CMake 错误, 终止所有处理过程;

比如使用message(WARNING "Compiler not supported C++ 20 standard"),出现下面结果

1
2
CMake Warning at ros/src/lqr_steering/CMakeLists.txt:15 (message):
Compiler not supported C++ 20 standard

换成AUTHOR_WARNING会出现下面结果

1
2
3
CMake Warning (dev) at ros/src/lqr_steering/CMakeLists.txt:15 (message):
Compiler not supported C++ 20 standard
This warning is for project developers. Use -Wno-dev to suppress it.

如果在执行cmake时,遇到一个错误情况需要停止执行,可以用FATAL_ERROR

1
2
3
if( SOME_COND )
message( FATAL_ERROR "You can not do this at all, CMake will exit." )
endif()

如果出现这种情况还要继续编译,那么就换成SEND_ERROR


指令最好全用大写
路径名中不要用\,而是用/,例如 include_directories(F:/Eigen)

常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 这必须是CMakeLists的第一行,限定cmake版本,Catkin需要2.8.3或更高版本
CMAKE_MINIMUM_REQUIRED(VERSION 3.3)

PROJECT #指定工程名称

add_compile_options(-std=c++11) # 支持C++ 11

set(CMAKE_BUILD_TYPE "Release") # 指定cmake版本为Release,或者用Debug
if(CMAKE_BUILD_TYPE STREQUAL "Release")
message(STATUS "build type is Release")
endif()

#SET 定义变量,可定义多个变量,例如: set(SRC_FILES main.cpp mainwindow.cpp mainwindow.h)
#MESSAGE 输出用户定义的信息

include_directories #指定头文件的搜索路径
link_directories #指定动态链接库的搜索路径
ADD_EXECUTABLE(bin_file_name ${SRC_FILES}) #生成可执行文件
add_definitions #添加编译参数 add_definitions("-Wall -g")
ADD_LIBRARY #生成动态库或静态库
link_libraries(lib1 lib2) #所有编译目标链接相同的库

SET_TARGET_PROPERTIES #设置输出的名称,设置动态库的版本和API版本
ADD_SUBDIRECTORY #向当前工程添加存放源文件的子目录

获取环境变量

获取bash.rc中的环境变量,通过ENV前缀来访问环境变量,读取环境变量使用$ENV{JAVA_HOME},查看环境变量:

1
message(STATUS " java home: $ENV{JAVA_HOME}" )

写环境变量如下:

1
set( ENV{PATH} /home/martink )

cmake判断操作系统和架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
message(${CMAKE_HOST_SYSTEM_NAME})
message(${CMAKE_HOST_SYSTEM_PROCESSOR})

if(CMAKE_HOST_SYSTEM_NAME MATCHES "Linux")
message("this is Linux")
elseif(CMAKE_HOST_SYSTEM_NAME MATCHES "Android")
message("this is Android")
endif()

if(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "aarch64")
message("this is aarch64 cpu")
elseif(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "x86_64")
message("this is x86_64 cpu")
endif()

获得include的文件路径

1
2
3
4
5
6
include_directories(
include
/usr/include/eigen3
)
get_property(dirs DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTY INCLUDE_DIRECTORIES)
MESSAGE("include path: ${dirs}")

指定生成文件的输出路径

使用SET命令重新定义EXECUTABLE_OUTPUT_PATHLIBRARY_OUTPUT_PATH变量来指定最终的二进制文件的位置

1
2
SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
SET(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)

上面的两条命令通常紧跟ADD_EXECUTABLEADD_LIBRARY,与其写在同一个CMakeLists.txt即可

include_directories 和 target_include_directories

都是添加头文件搜索路径。差别:

  • include_directories的影响范围最大,可以为CMakelists.txt后的所有项目添加头文件目录。一般写在最外层CMakelists.txt中影响全局

  • 影响范围可以自定义,加关键子PRIVATEPUBLIC。一般引用库路径使用这个命令,作为外部依赖项引入进来,target是自己项目生成的lib。

PRIVATE:target对应的源文件使用
PUBLIC:target对应的头文件、源文件都使用

如果有不同目录相同名称的头文件会产生影响,所以这里建议针对特定的 target 使用 target_include_directories

参考:
CMake基本教程
子工程和链接静态库
CMake使用进阶


shell命令 — 字符串相关

type -a 命令可以查看某个shell命令的含义,对alias也适用

echo带颜色的文本

需要使用参数-e,格式为echo -e "\033[字背景颜色;文字颜色m字符串\033[0m",比如"\033[45;5m 闪烁效果 \033[0m"

1
2
3
4
5
6
echo -e "\033[40;37m 黑底白字 \033[0m"
echo -e "\033[41;37m 红底白字 \033[0m"
echo -e "\033[42;37m 绿底白字 \033[0m"
echo -e "\033[43;37m 黄底白字 \033[0m"
echo -e "\033[44;37m 蓝底白字 \033[0m"
echo -e "\033[45;37m 紫底白字 \033[0m"

\033[5m 闪烁效果,这个以前不知道

1
2
3
4
5
6
7
8
9
字背景颜色范围: 40--49                   字颜色: 30--39 
40: 黑 30: 黑
41:红 31: 红
42:绿 32: 绿
43:黄 33: 黄
44:蓝 34: 蓝
45:紫 35: 紫
46:深绿 36: 深绿
47:白色 37: 白色

对shell脚本进行语法检查

1
bash -n script_name.sh         //   -n选项只做语法检查,而不执行脚本。

例如,检查结果可能是这样:

1
2
3
# bash -n t.sh 
t.sh: line 6: syntax error in conditional expression: unexpected token `;'
t.sh: line 6: syntax error near `;'

连续执行命令

连续执行shell命令有三种情况:

  • 用分号;间隔,会一直执行,无论命令对错。 echo abc; echo 123
  • &&间隔,执行到错误命令会停止。echo abc && adf && echo 123
  • ||间隔,执行到正确命令会停止。

二元比较操作符,比较变量或者比较数字.

整数比较

1
2
3
4
5
6
-eq 等于        if [ "$a" -eq "$b" ] 
-ne 不等于 if [ "$a" -ne "$b" ]
-gt 大于 if [ "$a" -gt "$b" ]
-ge 大于等于 if [ "$a" -ge "$b" ]
-lt 小于 if [ "$a" -lt "$b" ]
-le 小于等于 if [ "$a" -le "$b" ]

比如 $a -eq 10

字符串比较

1
2
= 等于        if [ "$a" = "$b" ] 
== 等于 if [ "$a" == "$b" ],与=等价


参考: shell脚本 if语句

字符串条件判断

1
2
3
4
if [ "$net" = "0" ]
then
echo "local"
fi

注意:

  • 左右方括号都要留空位,if后面也要有空位
  • Shell下使用等号赋值时,左右两边不能有空位
  • 一个等号和两个等号都可以
  • then要另起一行,以if开头,以fi结尾

查找字符串

grep -r "struct event_base" -n
在当前目录查找字符串,找到后返回文件和对应的行

空格的有和没有

定义变量时, =号的两边不可以留空格

1
gender=femal

条件测试语句 [ 符号的两边都要留空格
1
if [ $gender = femal ]; then

条件测试的内容,如果是字符串比较的话, 比较符号两边要留空格

1
if [ $gender = femal ]; then

如果if 和 then写在同一行, 那么,注意, then的前面要跟上 ; 号.如果 then 换行写, 那么也没问题.

1
2
if [ $gender = femal ]
then

命令和其后的参数或对象之间一定要有空格

1
if [ -x"~/Workspace/shell/a.sh" ];then

取字符串的某一段

1
2
3
4
5
6
7
#/bin/bash -e

ip="192.168.0.123"
net=$(cut -d'.' -f3<<<"192.162.0.123") # 3个<
sub=$(echo $ip | cut -c1-7) # -c 一般用于文件,比如 cut -c1-4 test.txt
echo $net
echo $sub

结果是:

1
2
0
192.168

cut命令一般用于处理表内容,例如只取某一列
-c:仅显示行中指定范围的字符;
-d:指定字段的分隔符,默认的字段分隔符为”TAB”;
-f:显示指定字段的内容;

使用awk的内建函数:

1
2
3
hostname -I | awk '{split($0,a,".");print a[1],a[2],a[3],a[4]}'   # 192 168 0 123

hostname -I | awk '{print substr($0,0,4)}' # 192.

所以如果想获得ip的网段,可以使用net=$(hostname -I | awk '{split($0,a,".");print a[3]}')

另一种截取字符串的方法

  • 从左边第几个字符开始,及字符的个数

echo ${var:0:5}

其中的 0 表示左边第一个字符开始,5 表示字符的总个数。

  • 从左边第几个字符开始,一直到结束。

echo ${var:7}

其中的 7 表示左边第8个字符开始,一直到结束。

  • 从右边第几个字符开始,及字符的个数

echo ${var:0-7:3}

其中的 0-7 表示右边算起第七个字符开始,3 表示字符的个数。
结果是:123

  • 从右边第几个字符开始,一直到结束

echo ${var:0-7}

表示从右边第七个字符开始,一直到结束。

左边的第一个字符是用 0 表示,右边的第一个字符用 0-1 表示

列出当前目录下,最大的10个文件

ls -Slh | head

连接字符串

如果想要在变量后面添加字符,可以用以下方法:

1
2
3
4
5
$value1=home

$value2=${value1}"="  # 用单引号也行

echo $value2

把要添加的字符串变量添加{},并且需要把$放到外面。这样输出的结果是home=,连接成功。

echo

echo换行

echo要支持同C语言一样的\转义功能,只需要加上参数-e

echo -e “\n” 就是换行
echo -e $(cat test.txt)
其中test.txt的内容:

1
aaa \nbbb

结果就是:
1
2
aaa
bbb

不要加双引号,那样会保留下来

echo文本到文件

echo换行的文本

1
2
3
echo -e "Hello\nworld"
echo -e 'Hello\nworld'
echo Hello$'\n'world

echo $(cat 1.txt) > 2.txt可以把文件1的内容复制到文件2。但如果文件1每行结尾没有加\n,文件2的内容不会换行。另外这样会覆盖文件2原有的内容。如果不想覆盖,而是追加到文件2,只要把>改为>>即可


fprintf,snprintf和sprintf函数

fprintf

原型:extern int fprintf (FILE *f,const char *s, ...);
可以将字符串输出到某文件中,但更常用的用法是:

1
2
fprintf(stdout,"Hello\n");	 //加换行
fprintf(stderr,"World!");

输出Hello换行World。stdout是行缓冲的,输出会放在一个buffer里面,只有到换行的时候,才会输出到屏幕。而stderr是无缓冲的,会直接输出。

snprintf

int snprintf(char *restrict buf, size_t n, const char * restrict format, ...);
函数说明:最多从源串中拷贝n-1个字符到目标串中,然后再在后面加一个0。所以如果目标串的大小为n的话,将不会溢出。
函数返回值:若成功则返回欲写入的字符串长度,若出错则返回负值。

1
2
3
4
5
6
7
8
9
char s1[6] = "12345";
char s2[6] = "67890";
int res;
res = snprintf(s1,sizeof(s1),"abcdefg"); //写入长度大于原来长度,写入abcde和\0,要求6
printf("s1:%s, res:%d\n",s1,res); //欲写入长度7,这个值是strlen,不包含\0
res = snprintf(s1,sizeof(s1),"abc"); //写入长度少于原来长度,则相当于替换
printf("s1:%s, res:%d\n",s1,res);
res = snprintf(s2,4,"%s","abcdefg"); //指定4,包含了\0
printf("s2:%s, res:%d\n",s2,res);

结果:
1
2
3
s1:abcde, res:7
s1:abc, res:3
s2:abc, res:7

sprintf

int sprintf(char *buffer, const char *format, [ argument] … );
函数功能:把格式化的数据写入某个字符串缓冲区。

函数不安全,写入目标时不会考虑字符数组的大小,要存储的字符超过数组长度时,会导致数组越界,编译不报错但运行报错,所以都会推荐使用snprintf.

1
2
3
4
5
6
char s1[6] = "12345";
int res;
res = sprintf(s1, "%s","abcdefg"); //超出原字符数组长度,不安全
printf("s1:%s, res:%d\n",s1,res);
res = sprintf(s1, "%s","abc");
printf("s1:%s, res:%d\n",s1,res);

结果:
1
2
s1:abcdef, res:7
s1:abc, res:3


new与delete

对类类型而言,new运算是先分配内存再执行构造函数,delete是先执行析构函数再释放内存

对array的用法

我们都知道new与delete经常这样用:

1
2
3
4
5
int* a = new int(10);
delete a;

int* b = new int[10];
delete []b;

如果第二种情况改用delete b;会不会有内存泄漏? 答案是仍然不会,分配简单类型内存时,内存大小已经确定,系统可以记忆并且进行管理,在析构时,系统并不会调用析构函数。它直接通过指针可以获取实际分配的内存空间,哪怕是一个数组内存空间。

但是对于类对象就不能这样用了,看下面的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Base
{
public:
Base()
{
std::cout<<"基类构造 "<<this<<endl;
}
virtual ~Base()
{
std::cout<<"基类析构"<<this<<endl;
}
};
Base* pb = new Base[5];
delete pb;

运行结果如下,有5个构造,但只有1个析构函数,而且从this指针来看,是数组第一个元素的析构函数:
1
2
3
4
5
6
7
基类构造 0xe118d4
基类构造 0xe118ec
基类构造 0xe11904
基类构造 0xe1191c
基类构造 0xe11934

基类析构 0xe118d4

delete pb只用来释放pb指向的内存和一个析构。delete[] pb用来释放指针指向的内存,还逐一调用数组中每个对象的析构。 所以为了编程规范,不管对简单类型还是类类型,都要用delete []pb的形式。

与malloc/free的区别

  • new/delete用于C++中的动态内存分配; malloc/free仅用于C环境,用于类类型时,不会运行构造析构函数

  • new/delete不必指定分配内存大小,malloc/free必须指定

  • new返回的是指定对象的指针,而malloc返回的是void*malloc的返回值一般都需要进行类型转化。

  • new是一个操作符可以重载,malloc是一个库函数

比如:

1
2
Base* b = (Base*)malloc(12);
free(b);

12是随便指定的,结果不运行构造和析构函数,而且如果中间运行成员函数,程序会崩溃。 所以这种代码没有任何意义


运算符重载(二)

以一个Point类为例,重载几个运算符,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Point
{
public:
Point(int x=0,int y=0)
{
arr[0] = x;
arr[1] = y;
}
void print()
{
cout<<"x: "<<arr[0]<<" y: "<<arr[1]<<endl;
}

int& operator [](int num)
{
assert(num==1 || num==0);
if(num==0)
return arr[0];
else if(num==1)
return arr[1];
}
Point& operator -()
{
return Point(-arr[0],-arr[1]);
}
Point& operator --()
{
return Point(arr[0]-1,arr[1]-1);
}
Point& operator ++()
{
return Point(arr[0]+1,arr[1]+1);
}
Point& operator +(Point& p)
{
return Point(arr[0]+p.arr[0], arr[1]+p.arr[1]);
}
Point& operator -(Point& p)
{
return Point(arr[0] - p.arr[0], arr[1] - p.arr[1]);
}
bool operator ==(const Point& p)
{
return ((arr[0]==p.arr[0]) && (arr[1]==p.arr[1]) )
}
private:
int arr[2];
};

有一个数组做成员变量,构造函数给数组赋值。
首先是一元运算符- -- ++,显然是无参数的。返回值应当是Point&,函数也容易理解。
二元运算符加法和减法及相等也简单,有一个参数而已。其实这三个运算符由于是双目的,最好按友元重载。

下标运算符重载的声明必须是返回类型& operator [](参数),只能作为类成员函数,不能做友元。

调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
Point p(1,4);
//改变 p
p[0] = 7;
p[1] = 9;
Point p1 = -p;
Point p2(4,12);
Point p3 = p - p2;
if(p2==p3)
cout<<"equal"<<endl;
else
cout<<"not equal"<<endl;
Point p4 = ++p3;
Point p5 = --p3;

参考:C++ 运算符重载


运算符重载(一)

不能重载的运算符:sizeof .(类成员访问) .*(类成员指针访问) :: ?:(三元运算符)。记住只有sizeof和带.的运算符不能重载。

运算符重载有两种方式:成员函数和友元函数。成员函数的形式比较简单,就是在类里面定义了一个与操作符相关的函数。友元函数因为没有this指针,所以形参会多一个。

对运算符重载通常是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Base
{
public:
Base();
explicit Base(int a)
{
m = a;
}
Base operator +(Base& b) //可以是成员函数,也可以是友元
{
Base temp;
temp.m = this->m + b.m;
return temp;
}
friend std::ostream& operator<<(ostream& out, Base& b); //只能是友元
private:
int m;
};

std::ostream& operator<<(ostream& out, Base& b)
{
out<<b.m;
return out;
}

调用:
1
2
3
4
Base b1(3);
Base b2(9);
Base b3 = b1+b2;
std::cout<<b3<<endl;

输入输出运算符重载不能做类的成员函数, 因为平时的输出命令是cout<<b;,实际是cout.operator<<b形式的,如果做成员函数,就变成了b.operator<<cout的形式。

C++ Primer的解释是:假设输入输出运算符是某个类的成员,则它们也必须是istream或ostream的成员。然而这两个类属于标准库,并且我们无法给标准库中的类添加任何成员。

Qt的<<重载也是类似的方法:

1
2
3
4
5
6
//输出QLineF的两点坐标
QDataStream &operator<<(QDataStream &stream, const QLineF &line)
{
stream << line.p1() << line.p2();
return stream;
}

  • 单目运算符最好重载为类的成员函数;双目运算符则最好重载为类的友元函数。
  • 以下一些双目运算符不能重载为类的友元函数:=、()、[]、->。
  • 类型转换运算符只能以成员函数方式重载
  • 流运算符只能以友元的方式重载

拷贝构造函数

基本规则

  • copy构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,没有返回值。它必须的一个参数是本类型的一个引用变量,如果形参是对象做值传递, 将实参传进函数时, 我们实际是拷贝一个副本,这样又要调用拷贝构造函数, 层层递归, 会把栈堆满。类中可以存在多个copy构造函数。

  • 编译器会自动生成默认copy构造函数,这个构造函数很简单,仅仅使用“老对象”的数据成员的值对“新对象”的数据成员逐个进行赋值,也就是浅拷贝

  • 默认copy构造函数不处理静态变量。如果静态成员变量在构造、析构实例的时候需要修改,那么通常需要手工实现copy构造函数和重载赋值运算符。

  • 如果对象存在了动态成员,那么需要手动实现析构函数,也就需要手动实现copy构造函数,因为默认copy构造函数使用的是浅拷贝,要改用深拷贝。

  • 如果派生类没有自定义拷贝构造函数,它在拷贝时,会调用基类的copy构造函数。如果两个类都自定义copy构造函数,那么只调用派生类的。

  • copy构造函数也要对常成员变量进行列表初始化

  • 基类定义了带参数的构造函数,派生类没有定义任何带参数的构造函数,则不能直接调用基类的带参构造函数,程序编译不通过

深拷贝

浅拷贝实际是对变量的引用,深拷贝是对类成员复制并重新分配内存, 二者的最大区别在于是否手动分配内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Base
{
public:
Base();
explicit Base(int a);
virtual ~Base();
Base(const Base& obj);
private:
int m;
// 最好用智能指针
int *p;
};
Base::Base(int a):
m(a)
{
p = new int(100); // p内存分配在stack
}

Base::~Base()
{
if(p)
{
delete p;
p = NULL;
}
}
Base::Base(const Base &obj):
m(0),
p(new int(100))
{
std::cout<<"copy构造函数"<<endl;
m = obj.m;
// 动态分配的内存必须自定义copy构造函数,浅拷贝不会处理
int* temp = new int;
*temp = *(obj.p);
this->p = temp;
}
//调用
Base b1(15);
Base b2(b1);

不能不处理p。假如copy构造函数中没有对p分配内存,编译正确,但运行时b2析构会出现问题。因为默认拷贝执行浅拷贝,把b2里的p也指向了b1里的p,二者地址相同,结果会出现二次析构,内存泄漏。

我用Creator试验二次析构,发现程序结束时报错 program has unexpectedly finished ,再次运行时Qt先报信息Fault tolerant heap shim applied to current process.,这就是内存泄漏造成的,按照这个方法解决

标准化容器使用insert、push、assign等成员增加元素的时候也会调用拷贝构造函数

禁用拷贝

禁用原因主要是两个:

  1. 浅拷贝问题,也就是上面提到的二次析构。
  2. 自定义了基类和派生类的copy构造函数,但派生类对象拷贝时,调用了派生类的拷贝,没有调用自定义的基类拷贝而是调用默认的基类拷贝。这样可能造成不安全,比如出现二次析构问题时,因为不会调用我们自定义的基类深拷贝,还是默认的浅拷贝。

Effective C++条款6规定,如果不想用编译器自动生成的函数,就应该明确拒绝。方法一般有三种:

  1. C++11对函数声明加delete关键字:Base(const Base& obj) = delete;,不必有函数体,这时再调用拷贝构造会报错
  2. 最简单的方法是将copy构造函数声明为private
  3. 条款6给出了更好的处理方法:创建一个基类,声明copy构造函数,但访问权限是private,使用的类都继承自这个基类。默认copy构造函数会自动调用基类的copy构造函数,而基类的copy构造函数是private,那么它无法访问,也就无法正常生成copy构造函数。

Qt就是这样做的,QObject定义中有这样一段,三条都利用了:

1
2
3
4
5
6
private:
Q_DISABLE_COPY(QMainWindow)

#define Q_DISABLE_COPY(Class) \
Class(const Class &) Q_DECL_EQ_DELETE;\
Class &operator=(const Class &) Q_DECL_EQ_DELETE;

类的不可拷贝特性是可以继承的,凡是继承自QObject的类都不能使用copy构造函数和赋值运算符

如果一个类的拷贝构造函数加了delete关键字,类名就不能作为函数的形参,可以改用指针或const引用

有没有定义派生类copy构造函数的情况的不同结果

先是没有定义派生类copy构造函数的情况:

1
2
Derived f;
Derived ff(f);

运行结果是这样:

1
2
3
4
5
6
7
8
Base constrct      0x75fd20
Derived construct 0x75fd20
Base copy constrct 0x75fd10
*************
Derived deconstruct 0x75fd10
Base deconstrct 0x75fd10
Derived deconstruct 0x75fd20
Base deconstrct 0x75fd20

对于副本的对象,只调用了基类copy构造函数。


然后是定义的情况,运行结果是这样:

先是f的基类构造和派生类构造,然后进入ff,这里的对象是个副本,所以this指针的地址不同了, 先是基类构造然后是派生类拷贝构造, 销毁时倒没什么特别。

这样看来, 派生类的copy构造函数可以尽量不定义。

参考:
详解copy构造函数
为什么很多人禁用拷贝(复制)构造函数


回调函数

之前对回调函数理解一直不到位,需要深入分析一下。
一般函数都是系统提供或程序员自定义的,让程序员使用的。但回调函数恰恰相反,它是程序员定义(注册),在特定条件(常常是用户触发)发生时由系统API调用的,是通过函数指针实现调用的。函数定义在高层,调用在底层。

Linux信号处理机制就是利用回调函数实现的,例如signaction某个形参就有一个成员是函数指针。

1
2
3
4
5
6
7
8
9
10
11
12
void func(int n)
{
printf("signal %d catched !\n",n);
}
int main()
{
struct sigaction act;
act.sa_handler = func;
sigaddset(&act.sa_mask,SIGQUIT);
act.sa_flags = 0;
sigaction(SIGINT,&act,NULL);
}

当程序运行后,按Ctrl+C会发送SIGINT信号,然后内核调用函数func,输出文本。

类的成员函数做回调函数

由于this指针的作用,使得将一个CALLBACK型的成员函数作为回调函数安装时就会因为隐含的this指针使得函数参数个数不匹配,从而导致回调函数安装失败。定义类成员函数时,在该函数前加CALLBACK即可将其定义为回调函数。

  1. 类的静态成员函数实现回调函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void func(int sig)
{
cout << endl;
cout << "signal: "<<sig<<endl;
if(sig == 15)
cout << "SIGTERM"<<endl;
else
cout << "SIGINT"<<endl;
exit(0);
}

class Foo
{
public:
static void func(int sig)
{
cout << endl;
cout << "signal: "<<sig<<endl;
if(sig == 15)
cout << "receive signal SIGTERM"<<endl;

exit(0);
}
};

int main()
{
signal(SIGINT, func);
signal(SIGTERM, Foo::func); // 第二个参数也可以是func
while(1) // 设法阻塞
sleep(1);
return 0;
}

从终端启动程序,会阻塞。按下Ctrl+C,会触发SIGINT信号。执行pkill命令,会触发SIGTERM信号。


this指针

this指针指向对象的地址,本身是一个常量指针MyClass *const this,也就是不能改变指向的对象。

this指针是在创建对象前就有了,在编译时刻已经确定,this指针放在栈上。当一个对象创建后,整个程序运行期间只有一个this指针.

我是这样理解类和this指针的:类相当于房子的户型,根据这个户型可以造出很多房子,这就相当于对象。每个房子的地址不同,这就相当于内存地址。当你进入一个房子后,你可以看见桌子、椅子、地板等,但是房子你是看不到全貌了。this就是房子里面的一个标识,说明了房子的地址,但这个标识又不占房子空间。

this指针不属于对象本身的一部分,不会影响sizeof作用。 顺便一提,一个空的类,sizeof的大小是1

this指针是编译器默认传给类中非静态函数的隐含形参,其作用域在非静态成员函数的函数体内。
在类的赋值运算符重载函数中,我们可以一般使用*this作为当前对象返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Base
{
public:
Base();
void test();
void test_pub();
}
//相当于void Base::test(Base* const this)
void Base::test()
{
std::cout<<this<<endl;
cout<<"base test"<<endl;
}
void Base::test_pub()
{
std::cout<<this<<endl;
cout<<"base test_pub"<<endl;
}

调用两函数的结果:

1
2
3
4
00DDF9A0
base test
00DDF9A0
base test_pub

也就是说运行时只有一个this指针。


类的静态变量和静态函数

如果有N个同类的对象,那么每一个对象都分别有自己的成员变量,不同对象的成员变量各自有值,互不相干。但是有时我们希望有某一个或几个成员变量为所有对象共有,这样可以实现数据共享。

可以使用全局变量,但用全局变量的安全性得不到保证,由于在各处都可以自由地修改全局变量的值,很有可能偶然失误,因此在实际开发中很少使用全局变量。

静态变量

  • 对于类的静态成员变量,只有static const int类型和 enum 类型能在类里面初始化,其他只能在.cpp里初始化,但不能在类体内初始化,因为静态数据成员为类的各个对象共享,否则每次创建一个类的对象则静态数据成员都要被重新初始化。

  • static 成员函数,声明时当然要加static,但是在cpp实现的时候不需要加static,否则报错

  • static成员变量的内存空间是在初始化时分配,程序结束才释放,跟类对象的销毁无关。

  • 静态变量不参与sizeof计算,因为它不占用对象的内存

  • 静态成员仍然遵循public,private,protected访问准则。

静态函数

  • 静态成员函数只能调用静态变量,因为没有this指针。 设计它的初衷是把类名当成namespace用,控制类内的static变量。

  • 静态成员函数仍然遵循访问等级,但最好是public,否则不能直接用类名::调用

  • 非静态成员函数可以任意地访问静态成员函数和静态数据成员。

  • 静态成员函数不能声明为虚函数,编译直接报错

  • 静态成员函数与成员函数不能同名同参数,也就是静态和非静态函数不能重载,否则编译器不知调用哪个

举例:

1
2
3
4
5
6
7
8
9
10
11
12
// 头文件
class Obj
{
private:
Obj() {}
static Obj* instance;

public:
static Obj* getInstance();
static const int n = 12;
static int n;
};

1
2
3
4
5
6
7
8
9
10
11
//源文件
Obj* Obj::instance=0; //类外定义
int Obj::n = 4; //类外定义
Obj* Obj::getInstance()
{
if(!instance)
{
instance = new Obj();
}
return instance;
}

参考:
Essential C++ 115页