0%

C语言学习笔记

一、C标准库

1. stdio.h

1.1. 字符串格式化输出函数 snprintf()

函数原型

1
2
3
#include <stdio.h>

extern int snprintf (char *__restrict __s, size_t __maxlen,const char *__restrict __format, ...)
  • snprintf是指定长度的格式化输出
  • 返回值为format拼接的长度,不是被写入后字符串的长度
  • 其中的__maxlen可以指定为sizeof(__s),snprintf只会写入__maxlen - 1的字符,并在最后拼接\0

1.2. 文件操作

1) 函数原型

1
2
3
4
5
6
7
8
9
10
11
12
13
FILE *fopen (const char *__restrict __filename, const char *__restrict __modes);

/**
* 从文件中读取固定长度内容
* @param __ptr 储存地址
* @param __size 元素大小
* @param __n 元素个数
* @param __stream 文件句柄
* @return 读取的元素个数
*/
size_t fread (void *__restrict __ptr, size_t __size, size_t __n, FILE *__restrict __stream);

int fclose (FILE *__stream);

2. string.h

2.1. 输出家族函数

1
2
3
4
5
6
#include <stdio.h>

int printf(const char *format, ...); //输出到标准输出
int fprintf(FILE *stream, const char *format, ...); //输出到文件
int sprintf(char *str, const char *format, ...); //输出到字符串str中
int snprintf(char *str, size_t size, const char *format, ...); //按size大小输出到字符串str中

以下函数功能与上面的一一对应相同,只是在函数调用时,把上面的…对应的一个个变量用va_list调用所替代。在函数调用前ap要通过va_start()宏来动态获取。

1
2
3
4
5
6
#include <stdarg.h>

int vprintf(const char *format, va_list ap);
int vfprintf(FILE *stream, const char *format, va_list ap);
int vsprintf(char *str, const char *format, va_list ap);
int vsnprintf(char *str, size_t size, const char *format, va_list ap);

format格式说明

1
2
3
4
5
6
7
8
9
//%s
const char *const str = "abcdefghijklmnopqrstuvwxyz";
const char *const str5 = "abcde";
printf("%.5s\r\n", str); //'abcde' 只显示前五个字符
printf("%.10s\r\n", str5); //'abcde' 不足10个只打印5个
printf("%5.3s\r\n", str); //' abc' 最少占用5个字符宽度,只打印3个,右对齐
printf("%-5.3s\r\n", str); //'abc ' 最少占用5个字符宽度,只打印3个,左对齐
printf("%3.5s\r\n", str); //'abcde' 最少占用3个字符宽度,打印5个,3的限制失效
printf("%10s\r\n", str); //'abcdefghijklmnopqrstuvwxyz' 最少占用10个字符宽度,打印26个,10的限制失效

2.2. 字符串拷贝 strncpy()

函数原型

1
2
3
#include <string.h>

extern char *strncpy (char *__restrict __dest, const char *__restrict __src, size_t __n)
  • strncpy拷贝字符串,会检测__src里面的结束符,只拷贝到结束符
  • 如果__src长度大于__n,将拷贝__n个字符到__dest,不会赋值结尾\0,需要手动赋值
  • 如果__src长度小于__n,将拷贝strlen(__src)个字符到__dest,剩余空间赋值\0

2.3. 初始化函数 memset()

1
2
3
4
5
6
7
8
9
10
#include <string.h>

/*
* @description 将n bytes起始地址为s的内存全部初始化为c
* @param __s 初始化地址
* @param __c 初始化的值
* @param __n 初始化的大小
* @return __s的地址
*/
void *memset (void *__s, int __c, size_t __n);

函数是按byte(8 bits)进行初始化的,每个字节均会被初始化为__c取前八位的值。

2.4. 字符串拷贝 strdup()

1
2
3
4
5
6
7
8
#include <string.h>

/*
* 拷贝字符串到空指针里,附带内存申请,注意指针原来指向的地址需要先free
* @_Src 要拷贝的字符串
* @return 申请后拷贝到的地址
*/
char *__cdecl strdup(const char *_Src);

2.5. 字符串比较函数 strcmp和strncpm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <string.h>

/*
* 比较字符串,返回比较的值
* @_Str1 要对比的字符串
* @_Str2 要对比的字符串
* @return 遇到'\0'或者不一样的字符串为止,第一个不一样的字符对比大小
* 'a' > '\0', 1;
* '\0' < 'a', -1;
* 'a' > 'b', 1;
* 'b' < 'a', -1;
* '\0' < '\0', 0;
*/
int __cdecl strcmp(const char *_Str1,const char *_Str2);

//多了大小限制,防止内存泄漏
int __cdecl strncmp(const char *_Str1,const char *_Str2, size_t _MaxCount);

3. setjmp

3.1. 非局部跳转函数 setjmp()/longjmp()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <setjmp.h>

/*
* @description 设置返回位置
* @param __env 保存状态信息的缓存地址
* @return 直接调用返回,0;调用longjmp后返回longjmp给入参数__val
*/
int setjmp (jmp_buf __env);

/*
* @description 跳转到setjmp的位置
* @param __env 跳转位置的状态信息
* @param __val 跳转位置返回值
*/
void longjmp (jmp_buf __env, int __val);

实例

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
#include <iostream>
#include <csetjmp>

using namespace std;

class Rainbow {
public:
Rainbow() {
cout << "Rainbow()" << endl;
}

~Rainbow() {
cout << "~Rainbow()" << endl;
}
};

jmp_buf kansas;

void oz() {
Rainbow rb;
longjmp(kansas, 47);
}

int main() {
int code = setjmp(kansas);
if ((code = setjmp(kansas)) == 0) {
cout << "Setjmp code " << code << endl;
oz();
} else {
cout << "Setjmp code " << code << endl;
}
}

输出

1
2
3
Setjmp code 0
Rainbow()
Setjmp code 47

注意事项

  • 在上述实例中发现类没有被析构,这两个函数可以实现跳转但是不会检测类相关,所以类不会被析构

4. dlfcn.h 动态链接库

4.1. dlopen()/dlsym()/dlclose() 打开/加载符号/关闭 动态链接库

示例用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int main() {
// 打开so库,句柄存缓存
auto handler = dlopen("./libtest.so", RTLD_NOW);
if (handler == nullptr) {
LOG_WARN("Can't open lib {}", "./libtest.so");
return -1;
}

// 加载函数parser
auto parser = (ParserFunc)dlsym(handler, "parser");
const char *dlsym_error = dlerror();
if (dlsym_error) {
dlclose(handler);
LOG_WARN("Cannot load symbol 'parser': {}", dlsym_error);
return -1;
}

// 调用函数
(void)parser();

dlclose(handler);
return 0;
}

4.2. 加载下一个动态库符号

  • 动态链接库符号查找是按照加载顺序查找的,比如有两个动态链接库有同一个init()函数,哪个库先加载,调用就会使用哪个库中的函数。
  • 如果想要使用下一个动态库中的符号,可以使用RTLD_NEXT作为handle传入
  • 使用RTLD_NEXT需要加编译选项-D_GNU_SOURCE

示例

  • 使用LD_PRELOAD=libtest.so xxx将二进制中调用动态库的函数log_init()hook掉,做一些自己的操作
  • 完成自己逻辑后,调回原来的函数
1
2
3
4
5
6
7
8
9
10
11
/**
* @brief hook的库函数,log_init
*
*/
extern "C" MYLIB_API int log_init(int pid, const char *szFileName) {
LOG_DEBUG("Hook log_init, pid %d, szFile %s", pid, szFileName);
...
LOG_DEBUG("call origin init");
auto origin_func = (func_logcInit)dlsym(RTLD_NEXT, "log_init");
return origin_func(pid, szFileName);
}

5. time.h 时间处理函数

5.1. tm指针释放的问题

  • tm指针在内部是一个固定的内存,每次调用都会修改此内存的值,外部不用释放
  • 但是需要考虑多线程和多次调用的问题,调用之后前一次结果就无效了

二、语法和类型

1. 32位和64位大小区别

  • short相当于short int
  • long相当于long int
  • size_t相当于unsigned long
  • long和指针大小都等于编译环境的位数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int main(int argC, char* arg[]) {
//32位编译
LOG_DEBUG("Size of void * %d", sizeof(void *)); //4
LOG_DEBUG("Size of float %d", sizeof(float)); //4
LOG_DEBUG("Size of double %d", sizeof(double)); //8
LOG_DEBUG("Size of char %d", sizeof(char)); //1
LOG_DEBUG("Size of short %d", sizeof(short)); //2
LOG_DEBUG("Size of int %d", sizeof(int)); //4
LOG_DEBUG("Size of long %d", sizeof(long)); //4
LOG_DEBUG("Size of long long %d", sizeof(long long)); //8

//64位编译
LOG_DEBUG("Size of void * %d", sizeof(void *)); //8
LOG_DEBUG("Size of float %d", sizeof(float)); //4
LOG_DEBUG("Size of double %d", sizeof(double)); //8
LOG_DEBUG("Size of char %d", sizeof(char)); //1
LOG_DEBUG("Size of short %d", sizeof(short)); //2
LOG_DEBUG("Size of int %d", sizeof(int)); //4
LOG_DEBUG("Size of long %d", sizeof(long)); //8
LOG_DEBUG("Size of long long %d", sizeof(long long)); //8
return 0;
}

2. char *achar a[]的区别

1
2
char *a = "Hello";
char a[] = "Hello";
char *a = "Hello";char a[] = "Hello";
字符串所在区域常量区
可读可写常量区不可写可读可写
赋值时刻编译时确定运行时确定
存取效率属于静态存储区,较慢存于栈上,较快
sizeof(a)指针的大小,取决于编译环境5 + 1,字符串长度加'\0'

3. const的使用

1
2
3
4
5
6
7
const char *p;          // *p是const,p可变:const 后面紧跟的是char,所以*p是一个char字符,不可变
const (char *) p; // p是const, *p可变:const 后面紧跟的是(char *)这个整体,所以p是char*类型,不可变。
char* const p; // p是const, *p可变:const 后面紧跟的是p,所以p不可变
const char *const p; // p和*p都是const:第一个const后面紧跟的是char,所以char类型的字符*p不可变;第二个const后面紧跟的是p,所以p不可变。
char const *p; // *p是const, p可变:const后面紧跟的是*, 但是单独的*不能表明修饰的内容,所以将*p看成一个整体,所以const修饰的是*p,*p不可变。
(char*) const p; // p是const, *p可变:const紧跟的是p,所以p不可变。
char const *const p; // p和*p都是const:第一个const紧跟的是*,不能表明修饰的内容,将后面整体的(* const p)看成一个整体,那就说明*p不可变,第二个const后面紧跟的是p,所以p不可变。

4. struct声明

4.1. 定义位变量

1
2
3
4
5
6
7
8
9
10
11
12
struct test_t {
uint16_t a : 1; // 0
uint16_t b : 1; // 1
uint16_t c : 1; // 2
uint16_t d : 1; // 3
uint16_t e : 2; // 4-5
uint16_t f : 2; // 6-7
uint16_t g : 2; // 8-9
uint16_t h : 2; // 10-11
uint16_t i : 2; // 12-13
uint16_t j : 2; // 14-15
};
  • 针对小端模式
  • 定义在前面的是低位,即a是16位中的0位
  • 但是小端模式下,低位在低地址,所以修改了a为1,则得到的是内存分布为0x010x00,但uint16_t打印还是0x0001

5. 宏定义

5.1. #if

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/********** ifdef形式 **********/
#ifdef WIN32
#else
#endif

#ifndef WIN32
#else
#endif

/********** if defined形式 **********/
// windows或苹果
#if defined(WIN32) || defined(__APPLE__)
// 要么没有定义__linux__,要么定义的__linux__为0
#elif !defined(__linux__) || !__linux__

#else

#endif

5.2. 预定义宏

1) 平台相关

(1) windows
  • WIN32: 由头文件minwindef.h定义,一般是判断是否有调用windowsapi
  • _WIN32: 32位和64位程序都有,由编译器指定
  • _WIN64: 只有64位程序才有,由编译器指定
(2) linux
  • gcc定义了下面的几个宏
1
2
3
4
5
6
7
8
9
=> gcc -dM -E - < /dev/null | grep -i linux
#define __linux 1
#define __gnu_linux__ 1
#define linux 1
#define __linux__ 1
=> gcc -dM -E - < /dev/null | grep -i unix
#define __unix 1
#define __unix__ 1
#define unix 1
  • clang定义
1
2
3
4
5
6
7
8
9
=> clang -dM -E -x c /dev/null | grep -i linux
#define __gnu_linux__ 1
#define __linux 1
#define __linux__ 1
#define linux 1
=> clang -dM -E -x c /dev/null | grep -i unix
#define __unix 1
#define __unix__ 1
#define unix 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
#if defined(_WIN32) || defined(WIN32)
//define something for Windows (32-bit and 64-bit, this part is common)
#ifdef _WIN64
//define something for Windows (64-bit only)
#else
// 32-bit only
#endif
#elif __APPLE__
#if TARGET_IPHONE_SIMULATOR
// iOS Simulator
#elif TARGET_OS_IPHONE
// iOS device
#elif TARGET_OS_MAC
// Other kinds of Mac OS
#else
// Unsupported platform
#endif
#elif __linux__
// linux
#elif __unix // all unix not caught above
// Unix
#elif __posix
// POSIX
#endif

2) 编译相关

(1) __stdcall/__cdecl/__fastcall/__pascal
__stdcall__cdecl__fastcall__pascal
参数压栈顺序从右到左从右到左左边两个大小不大于4个字节(DWORD)的参数放在ECX和EDX寄存器,其余的参数从右到左从左到右
栈清理方被调用的函数调用函数被调用的函数被调用的函数
函数名前缀_,名称后跟@和参数列表字节数的十进制
int func(int a, double b) => _func@12
前缀_前缀@,名称后跟@和参数列表字节数的十进制
int func(int a, double b) => @func@12
函数名全部大写
备注winapi使用,一般自清理堆栈是减少调用者大小C/C++默认调用方式,由于外部清理,一般作为可变参数函数使用前两个参数放入寄存器,更快调用,一般给参数很少的函数使用使用__stdcall代替

6. static和inline

  • static代表声明函数作用域,函数声明成static,将会仅在一个c文件生效,不会生成符号,外部不可调用
  • inline是类似宏的形式,建议编译器将其在调用处展开,但是仅有建议,是否真正当作inline还是看编译器分析结果
    • 如果函数过长或者存在递归等,就不会展开,仅会当作正常函数编译,还会生成符号,除非添加static

7. volatile

  • 编译器会对某些变量优化,可能存在将变量读入寄存器后,之后取值直接从寄存器取
  • 使用volatile要求编译器对变量的取值每次都从内存中取,不要使用cpu缓存
1
2
val1 = x;
val2 = x;
  • 如上面的实例,如果x没有被设置volatile,val2的赋值会被编译器优化成从寄存器取值,如果其他地方在val1赋值之后修改了值,val2拿不到修改后的值

8. register

  • 函数内使用,将变量存放到cpu寄存器中,不需要存放到内存,用于加快速度,和上面的volatile对应
  • 对register变量无法取地址,因为此变量在cpu寄存器中,会随时被更改

9. 数组

9.1. 数组的地址

  • 数组是一种类型,对数组取地址,仅仅是1级指针,指向数组这个类型,实际值和数组首地址一样,但是类型不一样
1
2
3
4
5
6
int arr[5] = {1, 2, 3, 4, 5};
auto a = arr; // int *
auto b = &arr; // int (*)[5]
auto c = &a; // int **
std::cout << ((void *)a == (void *)b) << std::endl; // 1
std::cout << ((void *)a == (void *)c) << std::endl; // 0

10. float

10.1. 组成和计算

  • float一共占用4个字节32位,1位符号位,8位二进制指数位,23位尾数
  • 转换过程是将数字转成二进制的带小数点的科学记数法,如

$$ 8.25 = 33 \times 2^{-2} = 100001 \times 2^{-2} = 1.00001 \times 2^3 $$

$$ 8.25 = 8 + 0.25 = 1000 + 0.01 = 1000.01 \times 2^3 $$

  • 小数点前面永远是1,所以直接省略掉,只取00001放到后面23位尾数内
  • 指数为5,由于8位可以表示-127 ~ 128,为了统一就直接取0为127,那么5就是 $127 + 3 = 130 = 0b10000010$
  • 前面符号位为0,正数,所以8.25的float为 0 10000010 000010000000000000000000

11. double

11.1. 组成

  • 计算参考float
  • double占用8个字节64位,1位符号位,11位指数位,52位尾数

三、系统相关

1. 大小端

1.1. 大小端出现的原因

  • 对于16位、32位、64位等寄存器,由于人们的读取习惯为从左向右,但是数字又是右边是低位,左边是高位,所以出现了不同硬件厂商定义了不同的结构模式

1.2. 大小端定义详解

  • 比如0x12340x12是高8位,0x34是低8位
  • 计算机需要从低地址数字低位开始读取,数字从小到大,0x340x12,x86和大部分arm架构都是小端模式
  • 人们却习惯从高位开始读,所以读成0x120x34,网络字节序定义为大端模式
0x0000000x000001
大端0x120x34
小端0x340x12

四、其他

1. exit和return

  • exit用于在程序运行的过程中随时结束程序,exit的参数是返回给OS的。main函数结束时也会隐式地调用exit函数。exit函数运行时首先会执行由atexit()函数登记的函数,然后会做一些自身的清理工作,同时刷新所有输出流、关闭所有打开的流并且关闭通过标准I/O函数tmpfile()创建的临时文件。exit是结束一个进程,它将删除进程使用的内存空间,同时把错误信息返回父进程;而return是返回函数值并退出函数。通常情况:exit(0)表示程序正常, exit(1)和exit(-1)表示程序异常退出,exit(2)表示表示系统找不到指定的文件。在整个程序中,只要调用exit就结束(当前进程或者在main时候为整个程序)。
  • return是语言级别的,它表示了调用堆栈的返回;return( )是当前函数返回,当然如果是在主函数main, 自然也就结束当前了,如果不是,那就是退回上一层调用。在多个进程时。如果有时要检测上个进程是否正常退出。就要用到上个进程的返回值,依次类推。而exit是系统调用级别的,它表示了一个进程的结束。
  • exit函数是退出应用程序,并将应用程序的一个状态返回给OS,这个状态标识了应用程序的一些运行信息。
  • 和机器和操作系统有关的一般是: 0为正常退出,非0为非正常退出;
  • exit()头文件为#include <stdlib.h>

2. 二进制符号

2.1. 查看符号

1
2
3
4
# 导出二进制符号表
readelf -Ws xxx > xxx.elf
# 导出汇编指令表
objdump -DS xxx > xxx.obj
  • 符号表中LOCAL代表本地符号,外部不可用。调用也会调用自己的函数,不可被普通hook
  • GLOBAL代表导出的符号,可以给外部调用。内部调用也是按照so库加载顺序调用,即可以被hook

2.2. extern “C” 作用

  • 一般这个标记是用来标识C++中导出的C函数,但是在符号表中的效果却不一样

Logc_init函数没有加extern "C"

1
2
3
4
5
...
70: 00000000000037c1 78 FUNC GLOBAL DEFAULT 11 subhook_free
71: 0000000000001c90 151 FUNC GLOBAL DEFAULT 11 _Z9Logc_initiPKc
72: 0000000000002ed1 75 FUNC GLOBAL DEFAULT 11 subhook_alloc_code
...

Logc_init函数加extern "C"

1
2
3
4
5
...
75: 000000000000386b 95 FUNC GLOBAL DEFAULT 11 subhook_remove
76: 0000000000001c80 151 FUNC GLOBAL DEFAULT 11 Logc_init
77: 0000000000002e74 77 FUNC GLOBAL DEFAULT 11 subhook_unprotect
...
  • 可以看到extern "C"使函数导出的符号按照C函数的方式,不加前缀和后缀

2.3. 隐藏符号

  • 二进制的符号可以通过strip命令进行隐藏
  • 默认so库的函数如果没加static都会被编译成外部可调用的形式,readelf可以看到是GLOBAL的形式
  • 但是如果不strip,动态库的符号可能会影响到其他动态库的引入。比如两个动态库内部都定义了一个log_init()函数,并且它们一个是内部使用的函数,不对外暴露,一个是给二进制用的函数。但是因为二进制运行加载顺序导致这个符号冲突了,导致和期望结果不一致。
  • 不对外暴露的接口最好隐藏掉符号。
  • 隐藏符号使用gcc编译选项-fvisibility=hidden,编译出来,所有函数默认是LOCAL形式,strip会隐藏掉
  • 但是需要暴露的符号需要在函数前面加上__attribute__((visibility("default")))
    • 加在头文件就可以了
1
2
3
4
5
6
7
#if WIN32
#define EXPORT_API __declspec(dllexport)
#else
#define EXPORT_API __attribute__((visibility("default")))
#endif

EXPORT_API int test();

编译

1
2
g++ -c libtest.cpp -o libtest.o -fvisibility=hidden -fPIC -Wall -Werror -std=c++11
...

2.4. 符号加载顺序

(1) 二进制正常加载

  • 调用的每个so库的符号,都会从前向后查找库中的符号,谁在前调用谁

(2) so库里面调用函数

  • 如果此函数是内部未导出函数,会调用自己
  • 如果此函数是导出函数,会重新从前向后查找,不会直接用自己

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// A.so
EXPORT_API void func1() {
printf("A.so func1\r\n");
func2();
}
EXPORT_API void func2() {
printf("A.so func2\r\n");
}

// B.so
EXPORT_API void func2() {
printf("B.so func2\r\n");
}

// main.cpp
int main() {
func1();
}
  • 如果,main.cpp链接A.so
1
2
3
4
5
6
=> LD_PRELOAD=./B.so ./main
A.so func1
B.so func2
=> ./main
A.so func1
A.so func2

五、glibc接口

1. popen 执行命令(跨平台)

1.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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* @brief 执行命令,返回结果
* @param[in] cmd 待执行命令
* @param[out] outStr 执行结果
* @param[out] ec 错误信息
* @param[in] exceptCodes 允许的一些错误,默认只允许成功
* @return 是否成功,错误信息由ec给出
*/
static bool cmdPopen(const std::string &cmd, std::string &outStr, std::error_code &ec,
const std::vector<int> &exceptCodes = {0}) {
std::string tag = "popen exec '" + cmd + "'";
LOGI(WHAT("{}", tag));
static const int RESULT_BUF_SIZE = 4096;
// 使用popen调用命令
auto fp = popen(cmd.c_str(), "r");
if (fp == nullptr) {
ec.assign(errno, std::system_category());
return false;
}

do {
// 读取popen的stdout结果
char resultBuf[RESULT_BUF_SIZE] = {0};
while (fgets(resultBuf, RESULT_BUF_SIZE, fp) != nullptr) {
outStr += resultBuf;
}
// 查看是否是读取出现错误
auto ret = ferror(fp);
if (ret != 0) {
LOGI(WHAT("{}, fgets error, ec {}, ignore it", tag,
std::to_string(std::error_code(ret, std::system_category()))));
clearerr(fp);
}
} while (false);

// 通过pclose获取是否执行失败
auto ret = pclose(fp);
fp = nullptr;
auto exitStatus = WIFEXITED(ret); // 进程是否退出,不是是否成功退出
auto err = WEXITSTATUS(ret); // 进程退出后的返回值,也就是errno

// 进程退出失败,仅打印日志
if (!exitStatus) {
LOGW(WHAT("{}, pclose failed"), REASON("process is not exit"), WILL("ignore it"));
}

// 判断是不是需要的错误信息
if (std::find(exceptCodes.begin(), exceptCodes.end(), err) == exceptCodes.end()) {
ec.assign(err, std::system_category());
return false;
}
// error被忽略,输出忽略信息给外面
outStr = "ignore errno:" + std::to_string(std::error_code(err, std::system_category()));
return true;
}

六、编译原理

1. 链接过程的重定位

踩坑记

1. redefine错误

  • 全局变量在定义必须在c和cpp中,可以在公用头文件以extern声明,不然会被两个同时包含头文件的源文件编译时报重复定义的错误
  • 宏定义在头文件中定义被两个源文件编译不会报重复定义的错误
  • 宏定义同一个名字在两个头文件定义,内容定义相同不报错,不同则报warning,最新的会覆盖掉老的定义

2. confiture模式的第三方库修改编译生成的库的名字

如libxml2、libcurl等第三方库,如果编译想要换个名字需要将

  • 当前目录及子目录的所有Makefile.amMakefile.in里面的libxxx.la修改成新的名字libdiy.la
  • 重新执行./configure && make即可

3. int赋值给unsigned long long的问题

  • 当int为负数时,赋值给unsigned long long时会将64位前32位变成1,由于补码特性
  • 正常取32位,需要使用unsigned int赋值

4. 头文件定义static的问题

  • 如果头文件定义了static变量(包括函数),效果是每个include的源文件都会存在static变量
  • 如果此变量作为进程唯一就会出问题,除非作为源文件唯一,否则需要定义在c文件中