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)

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} ) # 同上面等价

如果在执行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使用进阶


ROS串口通信(一)
abstract Welcome to my blog, enter password to read.
Read more
C++中的随机数

rand()函数可以用来产生随机数,但是这不是真正意义上的随机数,是一个伪随机数,是根据一个数(我们可以称它为种子)为基准以某个递推公式推算出来的一系列数,当这系列数很大的时候,就符合正态公布,从而相当于产生了随机数,但这不是真正的随机数,当计算机正常开机后,这个种子的值是定了的,除非你破坏了系统。

常用的代码:

1
2
3
4
5
6
7
8
9
10
11
#include <stdlib.h> 
#include <time.h>
using namespace std;
int main()
{
srand(time(NULL));
for(int i = 0; i < 10;i++ )
cout << rand() << '\t';
cout << endl;
return 0;
}

srand()用来设置rand()产生随机数时的随机数种子。参数seed必须是个整数,如果每次seed都设相同值,rand()所产生的随机数值每次就会一样。所以常用time(NULL)为参数(当前的UTC时间),这样每次生成的随机数就不同。

Qt中的qrand和qsrand用法一样,就不重复了。


Linux系统函数

exit函数

exit中的参数exit_code为0代表进程正常终止,若为其他值表示程序执行过程中有错误发生。出错时退出一般用exit(EXIT_FAILURE);

system()函数

这个函数是调用/bin/sh执行脚本的,有些命令如rosrun不能在/bin/sh下执行

WIFEXITED(status) 这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值。当WIFEXITED返回非零值时,我们可以用这个宏来提取子进程的返回值,如果子进程调用exit(5)退出,WEXITSTATUS(status)就会返回5;如果子进程调用exit(7),WEXITSTATUS(status)就会返回7。请注意,如果进程不是正常退出的,也就是说,WIFEXITED返回0,这个值就毫无意义。

所以一个典型的system函数的使用是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pid_t status;
std::string cmd = "rosnode kill /lidar";
status = system(cmd.data());
if (-1 == status) // 语句有错
{
return -1;
}
else
{
ROS_INFO("WIFEXITED return: %d",WIFEXITED(status));
if (WIFEXITED(status)) //返回一个非零值, 正常退出
ROS_INFO("child process exit done: %d", WEXITSTATUS(status) );
else
ROS_INFO("child process exit abnormally");
}

errno

errno表示错误代码。 记录系统的最后一次错误代码。代码是一个int型的值,在errno.h中定义。系统每一次出错都会对应一个出错代码,例如12表示“Cannot allocate memory”。

stderr和fprintf函数

linux中的进程启动时,都会打开三个文件:标准输入、标准输出和标准出错处理。通常这三个文件都与终端联系。这三个文件分别对应文件描述符0、1、2。系队统自定义了三个文件指针stdin、stdout、stderr,分别指向标准输入、标准输出和标准出错输出。stderr是linux标准出错的文件指针,定义为extern struct _IO_FILE *stderr;,对应文件描述符2,通常结合fprintf使用:

1
fprintf(stderr,"error message");	//不必加换行

STDERR_FILENO和write函数

1
2
3
4
/* 文件描述符*/
#define STDIN_FILENO 0 /* Standard input. */
#define STDOUT_FILENO 1 /* Standard output. */
#define STDERR_FILENO 2 /* Standard error output. */

与上面用法类似,但write的首个参数是文件描述符:

1
2
char err[] = "error\n";
write(STDERR_FILENO,err,strlen(err)); //输出error(换行)

perror函数

需要包含头文件stdio.h,perror是错误输出函数,在标准输出设备上输出一个错误信息,是对errno的封装。perror(“fun”),其输出为:fun:后面跟着错误信息(加一个换行符)。

1
perror("status:");	//不报错时,输出 status:Success

strerror函数

stderror是通过参数errno,返回错误信息:printf("strerror: %s\n",strerror(errno));

atexit函数

功 能: 注册终止函数(即main执行结束后调用的函数)

用 法: int atexit(void (*func)(void)),也就是只能注册形参和返回值都为空的函数

exit函数和_exit函数都可以退出程序,但后者是立即进入内核,前者是做一些清理处理再进入内核.atexit函数就是用于执行清理时的一些操作.

exit调用终止处理函数的顺序和atexit登记的顺序相反.程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void  test1()
{
cout<<"test 1"<<endl;
}

void test2()
{
cout<<"test 2"<<endl;
}

void test3()
{
cout<<"test 3"<<endl;
}

int main()
{
atexit(test1);
atexit(test2);
atexit(test3);
return 0;
}

运行结果:
1
2
3
test 3
test 2
test 1

gethostname函数

功能是获得计算机主机名,形参分别是char*和字符长度,成功会返回0

1
2
3
4
char name[40];
memset(name,0,sizeof(name));
if(!gethostname(name,sizeof(name) ) )
printf("%s\n\n",name);


memset和memcpy函数

memset函数

原型:void *memset(void *s, int ch, size_t n);
作用:将s所指向的内存中的前n个字节的内容全部设置为ch指定的ASCII值,这个函数通常为新申请的内存做初始化工作。一般用于结构体和数组的初始化。

  1. memset中的第三个参数一定要使用sizeof操作符,因为每个系统下对类型长度的定义可能不一样。
  2. memset中的第一个参数一定要是一个已知的、已经被分配内存的地址,否则会出错。
  3. 对于单字节数据类型(char)可以初始化为任意支持的值,都没有问题,但是对于非多字节数据类型只能初始化为0,而不能初始化成别的初值,否则容易出错。

memset的效率很高,比手动赋值要高的多,比bzero也要高,尤其大数组的情况。

我是这样实现的:

1
2
3
4
5
6
7
8
9
10
11
void* _memset(void* dst,int val, size_t count)
{
assert(dst!=NULL);
char* tmpdst = (char*)dst;
while(count--)
{
*tmpdst = (char)val;
tmpdst++;
}
return dst;
}

memcpy函数

memcpy函数的使用场合是不需要考虑内存重叠问题的,因为涉及到内存重叠时我们应该调用的是memmove函数。我是这样实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void* _memcpy(void* dst, const void* src, size_t n)
{
assert(dst!=NULL && src!=NULL);
assert(n>=0);
char* temp = (char*)dst;
const char* p = (char*)src;
size_t m=0; //void指针不能自增
while(n--)
{
*temp = *p;
temp++;
p++;
}
return dst;
}

memmove进行了改进,考虑了内存重叠的情况:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void* my_memmove(void* dst, const void* src, size_t n)
{
char* s_dst;
char* s_src;
s_dst = (char*)dst;
s_src = (char*)src;
if(s_dst>s_src && (s_src+n>s_dst)) {
s_dst = s_dst+n-1;
s_src = s_src+n-1;
while(n--) {
*s_dst-- = *s_src--;
}
}else {
while(n--) {
*s_dst++ = *s_src++;
}
}
return dst;
}


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]}')

列出当前目录下,最大的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,只要把>改为>>即可


不要显式调用构造函数和析构函数

常用的代码如下:

1
2
Base b;
cout<<"hello"<<endl;

结果:
1
2
3
基类构造 0x62fe84
hello
基类析构0x62fe84

但是这样的代码就不同了:

1
2
Base();		// 匿名的临时对象
cout<<"hello"<<endl;

运行结果:
1
2
3
基类构造 0x62fe84
基类析构0x62fe84
hello

可见临时创建的类对象立刻销毁了,这与平时创建在stack上的对象再出了局部范围再销毁是不同的。

看这样的代码,是关于显式调用构造函数导致的成员变量未初始化问题:

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
class CTest  
{
public:
CTest()
{
m_a = 1;
}
CTest(int b)
{
m_b = b;
CTest();
}
~CTest() {}
void showA()
{
std::cout<<"a:"<<m_a;
}
private:
int m_a;
int m_b;
};

void main()
{
CTest myTest(2);
t.showA();
}

结果是a:6487936,也就是m_a未初始化。这里我们创建的对象是myTest,希望对其成员m_a初始化,但在构造函数里显式调用另一个构造函数,实际上是创建了一个临时对象,这个对象对m_a初始化了,这跟myTest是没有关系的,而且它很快又销毁了,所以没有达到目的。

这样的代码也是有问题的:

1
2
3
Base b;
b.~Base();
cout<<"hello"<<endl;

结果是二次析构了:

1
2
3
4
基类构造 0x62fe84
基类析构 0x62fe84
hello
基类析构 0x62fe84

所以不要显式调用构造函数和析构函数,这是危险的。


C++中的跟踪调试宏和变量

根据编译器的情况,有下面的宏用于调试程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//判断是不是C++环境,需要注意的是ROS环境中这里为否
#ifdef _cplusplus
printf("C++\n");
#endif
//判断是不是C环境
#ifdef __STDC__
printf("C\n");
#endif
//输出语句所在函数,行,文件等参数
printf("function %s: Line 25\n",__func__); //或者用__FUNCTION__
printf("pretty function %s: Line 25\n",__PRETTY_FUNCTION__); //函数声明,包括了参数
printf("line: %d\n",__LINE__);
printf("current file: %s\n",__FILE__);
printf("date: %s\n",__DATE__);
printf("time: %s\n",__TIME__);


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


char数组和指针问题

这个问题是C++基础问题中相当折腾人的一个,死记硬背解决不了根本问题,记住还是要忘,需要仔细研究其本质。

这两种方式就是数组和指针的方式:

1
2
char a[6] = "abcde";
char *b = "abcde";

第一行声明了并初始化了一个char数组,第二行是声明char指针b,指向了常量字符串。其中a是数组的首地址,a和b的地址一定不同。

千万不能说数组名是指针,可以用sizeof来否定:

1
2
char a[]="abcde";
cout<<sizeof(a)<<endl;

如果a是个指针,那么结果是4,但结果是6.

数组不能被直接复制,所以当数组名作为函数参数的时候,要么就是数组的引用,要么就是指向第一个元素的指针,他们的值是相等的。当你对一个数组做&的时候,他提取的是指向数组的指针,然后仍然可以隐式转换成指向第一个元素的指针,而且它们的值是相等的。

这样的代码是错的:

1
2
char a[6] = "abcde";
a[6] = "asdfge";

只有声明里才能用a[6],这就好比int a[6]={1,2,3,4,5,6};,但不能再用a[6]={5,6,7,8,9,0};。应该用a[0]='A';

这样的代码是正确的:

1
2
3
char a[]="abcde";
char *b;
b = a;

b是指针变量,指向了数组的首地址。

这样是错的:

1
2
3
char *a="abcde";  或者  char a[]="abcde";
char b[6];
b = a;

实际上是上一种情况的相反,报错error: incompatible types in assignment of 'char*' to 'char [6]'因为不存在一个隐式转换使得 char 被转换成 char[]。这个问题比较关键,我们可以把数组名b理解成一个常量指针,它不能指向其他地址,但指向的字符串可以改变。但是注意只是这么理解而已,数组名并不真的是常量指针。同样的,b++;也是错的。
从另一个角度来看,*数组名做函数参数时会退化为指针
,这里没有退化为指针的条件,所以b不能当指针变量用。

这样也是错的:

1
2
char a[6];
a = "abcde";

a是数组的首地址,怎么把字符串常量赋给它?

再看这种情况:

1
2
3
char *a, *b;
a = "abcde";
b = "abcde";

a和b的值不一定相同,也就是不一定是同一个地址,这取决于编译器的行为。

对于char指针和数组,以下操作都是可行的。

1
2
3
4
5
6
7
8
9
10
11
const char* p="abcd";	//在常量区,应当加const,否则编译器会报警
// char p[]="abcd"; // 在stack
cout<<p<<endl; // abcd
cout<<&(*p)<<endl; //abcd
cout<<*p<<endl; // a
cout<<&p<<endl; // 0x62fe9c

cout<<p+2<<endl; //cd
cout<<*(p+2)<<endl; //c
cout<<*p+2<<endl; //99
cout<<p[2]<<endl; //c

但下面操作仅适用于char数组,不能用于指针,指针指向的是常量:

1
//p[0]='A';

对于数组char p[]="abcd";,可以使用p[2],这是因为数组名在这里退化为指针,p转为指向数组首元素的char*类型。也就是说指针本身就可以用[],反而是数组名需要先转换为指针才能用[]。看下面的例子:
1
2
3
4
5
6
int b[5] = {1,2,3,4,5};
int *f=b;
cout<<f[2]<<endl; // 3
f[2]=0;
cout<<f[2]<<endl; // 0
cout<<b[2]<<endl; // 0

对于常量字符串,都可以用数组下标,这种做法比较少见:

1
cout<<"abcd"[2];     //c