大学的时候学习过c语言,不过一直都没怎么用过,到现在基本已经都还给老师了。所以想着找个时间补补课,所以在网上找了一个c语言课程,跟着老师一起学习了下。
ps: 上班之后才觉得花钱买课一点都不是负担,只是没有时间和精力去学习。
1. 搭建环境
这里使用的是window10环境。
- 安装msys2包管理工具,官网下载exe安装就好。
- 安装visul sudio, 选择社区版本就好了
-
- 安装clion,使用的是2020.3
- 安装mingw。打开msys2,使用命令安装依赖
pacman -Syu
pacman -Su
pacman -S base-devel
pacman -S mingw-w64-x86_64-toolchain
如果在配置clion的时候发现安装的mingw版本太新,clion不支持,可以使用官网的工具安装mingw。安装时选择架构x86_64。
2. clion配置
配置clion的toolchain和cmake的debug。点击+号来添加,vs会自动搜索到路径,mingw选择自己安装的路径。
在配置上除了刚才的编译链的设置,还可以设置代码风格,通过设置editor->inspecting和editor->code stype->c/c++
.
3. 数据类型
3.1 int类型
- 不同的编译器会有不同的实现,比如int, long, short, long long的实际长度,不同编译器可能不同。
- 无符号整型前面加 unsigned
- size_t 其实是个别名
- <limits.h>包里定义了不同类型的最大值,最小值
3.2 变量
&号取地址,在watcher上使用ctrl+enter现实在内存分析中
C语言和golang一样,如果定义了变量就会有一个初值,会分配一块内存。这个初始值是不一定的,全局变量可能是固定的,但是局部变量是随机的。
3.3 常量
常量const 声明了只读变量,无法通过=直接赋值,但是可以拿到指针之后直接对内存进行赋值,所以实际上也是可以改的。
使用宏定义能实现真正的常量,宏会在预编译时被全部替换成字面值。真正的常量也就是字面值,尽量不要在代码中使用硬编码。
非0表示真,0表示假
使用位运算定义一些开关比较好用。
输入函数
int i;
scanf("%d", &i); //如果有转换错误不会报错
4. 函数
4.1 函数定义
函数参数列表为空时,表示参数个数不一定。如果参数列表为void,表示没有参数。
在Java或者c++中,不写参数列表表示没有参数。
函数命名使用Pasaca方式
函数原型就是提供一个函数声明,在调用时能够使用,具体实现是在编译之后提供。
4.2 变量初始值
自动变量,在作用域内定义时自动分配内存。
auto int value = 0; //auto表示变量类型,int表示变量数据类型
在msvc编译中,非静态局部变量不设初值,默认值是0xcccccccc
,“烫”的GBK编码就是cccc,所以会有“烫烫烫”。在mingw中不存在这个问题。
4.3 变长参数
在参数列表里使用...
表示,需要传入参数个数。在函数中使用va_list
来遍历获取参数列表然后使用。
对比而言,Java里的变长参数就等同于数组,可以按照数组的方式使用。
5. 宏定义
5.1 编译
源代码(预处理器)宏替换后的源代码(编译器)中间文件(链接器)可执行程序
中间文件缺少的是最终执行的函数符号地址等
引入头文件就是引入了函数原型和预定义变量等
gcc -E ./xxx.c -o xxx.c //输出预编译结果
5.2 宏函数
会把语句替换到所有的调用地方,在调用时不要对传入参数产生影响
#define MAX(a, b) (a) > (b) ? (a) : (b) //使用(a)避免嵌套使用时混乱
宏函数类型无关,编译是否通过完全看替换之后程序是否正确
条件编译,可以判断头文件是否被引入过,避免多次引入
#ifndef CHAPTER1_INCLUDE_HUAHUA_H_
#define CHAPTER1_INCLUDE_HUAHUA_H_
void SayHello(void);
#endif //CHAPTER1_INCLUDE_HUAHUA_H_
可以在CMakeLists里,添加宏定义到可执行文件
target_compile_definitions(${name} PUBLIC DEBUG) //这样就相当于定义了`DEBUG`宏
宏函数实现PRINT
//按照格式打印字符串并自动换行
#define PRINTF(format, ...) printf(format"\n", ##__VA_ARGS__)
//打印出变量名:变量值
#define PRINT_INT(value) PRINTF(#value": %d", value)
宏定义中其他有用的变量
__FILE__ //文件名
__LINE__ //行号
__FUNCTION__ //函数名
//打印出文件行号,
#define PRINTFF(format, ...) printf("("__FILE__":%d) %s : "format"\n", __LINE__, __FUNCTION__, ##__VA_ARGS__)
字符串直接拼接
//"hello"" world" = "hello world"
printf("%s", "hello"" world\n");
char *s = "hello ""world";
printf("%s", s);
6. 编译过程
6.1 编译过程
在安装mingw的目录下找到mingw64.exe,打开后进入命令行窗口。
预编译,把宏定义展开加入到源文件中
gcc -E hello.c -o hello.i
编译成汇编
gcc -S hello.i -o hello.s
编译成一个目标文件
gcc -C hello.s -o hello.o
目标文件不能直接执行,需要链接到标准库等才能
pe window可执行文件 elf linux编译文件格式
ld hello.o //显示缺少的库文件
转换为可执行文件,会自动链接各种需要的库文件
gcc -v hello.o -o hello
静态链接,在编译的时候确定外部符号的地址,并将依赖的符号对应的文件编译到一起,形成可执行文件,扩展名.a
动态链接,在运行时动态加载链接库,扩展名.so
6.2 静态库
创建两个头文件和两个库文件
gcc -c huahua_byebyte.c huahua_hello.c
ar rcs huahua.a huahua_byebyte.o huahua_hello.o
ar t huahua.a
ar命令其实就是把生成的o文件打包了
使用静态库,链接打包的o文件
gcc hello_huahua.c helloutils/huahua.a -o huahua
6.3 动态链接库
创建动态库
ld 查看依赖的静态库
ldd 查看动态库
window需要加引入库参数,Linux不需要,格式也不一样
gcc -shared -Wl,--out-implib,libhuahua.a -fPIC huahua_hello.c huahua_byebyte.c -o libhuahua.dll
objdump -t huahua.dll | grep huahu
使用
//L 指定路径 l指定库名字
gcc hello_huahua.c -Lhelloutils -lhuahua -o hello_huahua
//window下查找动态库先从当前目录下查找
6.4 使用CMake编译静态库和动态库
编译动态库的写法
cmake_minimum_required(VERSION 3.10.2)
project(hello C)
set(CMAKE_C_STANDARD 11)
#如果是window,还要添加导出库
if (WIN32)
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS 1)
endif (WIN32)
#添加动态共享库
add_library(hello SHARED
huahua_hello.c)
#把不同类型文件安装到不同目录下
#RUNTIME 可执行文件,window会把动态库也加在这里,否则无法运行
#LIBRARY 库文件,unix会把so文件放在这里
install(TARGETS hello
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib/static)
编写静态库
cmake_minimum_required(VERSION 3.10.2)
project(byebye C)
set(CMAKE_C_STANDARD 11)
#直接添加到工程中
add_library(byebye STATIC
huahua_byebyte.c)
主工程cmake
文件
cmake_minimum_required(VERSION 3.17)
#获取项目目录名称
get_filename_component(ProjectId ${CMAKE_CURRENT_SOURCE_DIR} NAME)
#把空格替换成下划线
string(REPLACE " " "_" ProjectId ${ProjectId})
#设置安装路径
set(CMAKE_INSTALL_PREFIX install)
project(${ProjectId} C)
set(CMAKE_C_STANDARD 11)
if (APPLE)
set(CMAKE_INSTALL_RPATH @loader_path/../lib)
elseif(UNIX)
set(CMAKE_INSTALL_RPATH \$ORIGIN/../lib)
endif ()
#添加子目录,会在下面继续查找CMakeList
add_subdirectory(hello)
add_subdirectory(byebye)
#把所有c文件添加到files
file(GLOB files "${CMAKE_CURRENT_SOURCE_DIR}/*.c")
foreach(file ${files})
get_filename_component(name ${file} NAME)
#添加到可执行文件中
add_executable(${name} ${file})
target_compile_definitions(${name} PUBLIC DEBUG)
#添加库路径
target_link_libraries(${name}
hello
byebye)
#把可执行文件输出到bin
install(TARGETS ${name} RUNTIME DESTINATION bin)
endforeach()
6.5 c++调用c程序
c和c++编译出的函数的描述符不一样,所以无法直接在c++里调用c程序
c语言对象的描述符
objdump.exe -t hello.dir/huahuautil/huahua_byebye.c.obj
hello.dir/huahuautil/huahua_byebyte.c.obj: file format pe-x86-64
SYMBOL TABLE:
[ 0](sec -2)(fl 0x00)(ty 0)(scl 103) (nx 1) 0x0000000000000000 huahua_byebyte.c
File
[ 2](sec 1)(fl 0x00)(ty 20)(scl 2) (nx 1) 0x0000000000000000 SayByeBye
c++对象的描述符
objdump.exe -t hello.dir/huahuautil/huahua_hello.cpp.obj
hello.dir/huahuautil/huahua_hello.cpp.obj: file format pe-x86-64
SYMBOL TABLE:
[ 0](sec -2)(fl 0x00)(ty 0)(scl 103) (nx 1) 0x0000000000000000 huahua_hello.cpp
File
[ 2](sec 1)(fl 0x00)(ty 20)(scl 2) (nx 1) 0x0000000000000000 _Z8SayHellov
调用这两个函数的c++程序的描述符,可以看到hello的符号是一样的,但是byebye的符号是不一样的,所以会报错找不到函数。
AUX scnlen 0xb8 nreloc 8 nlnno 0
[ 37](sec 0)(fl 0x00)(ty 20)(scl 2) (nx 0) 0x0000000000000000 __main
[ 38](sec 0)(fl 0x00)(ty 20)(scl 2) (nx 0) 0x0000000000000000 _Z8SayHellov
[ 39](sec 0)(fl 0x00)(ty 20)(scl 2) (nx 0) 0x0000000000000000 _Z9SayByeByev
在头文件里函数名之前添加声明,告诉程序如果是c++程序则以c的形式进行添加byebyte函数
#ifdef __cplusplus
extern "C" {
#endif
void SayByeBye(void);
#ifdef __cplusplus
}
#endif
c++filt 命令能查看符号对应的原始语句,比如c++filt _Z9SayByeByev
7. 数组
数组定义
类型 数组名[数组大小]
初始化
1. 大括号直接赋值
2. 下标赋值
边界
c语言里数组没有下标越界错误,下标只是表示地址偏移
golang 里面只有运行时检查数组越界
长度声明,VLA变长数组
c数组长度声明中必须是常量,不可以是变量
从c99开始支持使用变量声明长度, gcc支持,,msvc不支持
const修饰变量
c语言中const表示只读变量,本质还是变量
c++中,const修饰的表示常量
字符串其实就是字符数组
//c语言中的字符串其实就是字符数组,加NULL,也就是\0
char string[] = "Hello World"; //数组长度实际为11,后面会自动加上\0
c语言中以数组作为函数参数,必须传入数组长度,因为数组类型本身不包含长度
Java等其他类型语言中,会占用一部分空间存储数组长度,而c语言更注重效率
c语言中的多维数组,本质上也是一维数组,多维数组初始化要指定类型长度
如果要使用数组作为函数返回值,要给函数传入一个外部数组,不能直接返回函数内定义的局部自动变量
8. 指针
指针保存变量地址
只读变量指针,只读指针变量
int const *p //p指针,指向只读int变量,不能对Int进行修改
int *const p //p指针只读,执行int变量,不能重新指向
特殊指针
NULL
指向0地址,表示空指针
野指针
指向无效地址的指针,比如函数调用中指向局部变量的指针,函数退出之后局部变量销毁,指针成为野指针
指针加减
指针加减相当于在进行地址移动,移动单位是指针指向的类型的字节数
int *p;
p = P + 1; //相当于向后移动int大小,4字节位置
指针和数组的关系
//数组名本质就是指针,数组下标取值本质是内存位置偏移
//以下操作都是等价的
int array[] = {1, 2, 3, 4, 5, 6, 7, 8};
int *p = array;
printf("%d\n", array[3]);
printf("%d\n", p[3]);
printf("%d\n", *(p + 3));
printf("%d\n", *(array + 3));
printf("%d\n", 3[p]);
printf("%d\n", 3[array]);
printf("%d\n", *(3 + p));
printf("%d\n", *(3 + array));
左值和右值
左值代表的是一块内存空间;右值代表是赋值给内存空间的数值
int a;
a = 1; //a是一块内存地址的引用,1是赋给内存的值
int *p = &a; //对a取地址后返回的是地址,变成了数值;p是一个新变量,即一块内存
&a = 3; //错误,对a取地址后返回的是地址,变成了数值
p + 1 = &a; //错误,p+1在做运算之后返回的是地址,是个数值
使用指针参数作为返回值
在函数参数列表中传入指针,接收返回值
1. 减少函数返回值复制,提高性能
2. 实现多返回值
动态内存分配
malloc(size_t) 在堆区分配指定大小的内存,单位字节,分配的内存不会被置零
calloc(count, size_t) 分配的内存会被置为0
realloc(void*, size_t) 重新分配一块内存,不会置零
分配的内存使用完毕之后需要手动调用free()进行释放
使用动态内存
避免修改指向已分配动态内存的指针
free之后的指针置为NULL
避免过多的指针指向同一块动态内存
动态内存遵循谁分配谁回收的原则
函数指针
//函数名是指向一段代码的指针,函数名就是一个函数指针;
int (*func)(int a, int b) = ∑
func(1, 2);
(*func)(2, 3);
(*Sum)(4, 5);
//返回int*的函数
int *f1(int a, int b);
//指向函数的指针,函数int (int, int),函数指针和函数名等价
int (*f2)(int a, int b);
//函数指针,函数int* (int, int)
int* (*f3)(int a, int b);
//函数指针,函数int[] (int, int), 语法上实际上不允许的
int (*f4)(int a, int b)[];
//函数数组,函数int (int, int), 语法上实际上不允许的
int (*f5)[](int a, int b);
//函数指针数组,指针指向函数int (int, int)
//注意:[]优先级比*高
int (*f6[])(int a, int b);
typedef 定义函数类型
typedef int (*Func) (int, int); //定义了一个类型是Func,是个函数指针类型
c类型鉴定网站
https://cdecl.org/
9. 结构体
9.1 定义
//使用typedef 给结构体起别名
typedef struct Hobby {
char* name;
} Hobby;
//定义结构体
struct Huahua {
char *name;
int age;
//匿名结构体
struct {
char *name;
char *location;
}company;
Hobby *hobby;
};
//访问成员变量使用.运算符,结构体指针使用->运算符,初始化使用.
9.2 内存对齐
//按照类型本身占用内存大小的倍数对齐
struct Huahua {
char *name; //4字节
int age; //对齐到4字节倍数
//匿名结构体
struct {
char *name;
char *location;
}company;
Hobby *hobby; //对齐到4字节倍数
};
//32位系统下的Huahua结构体的内存分布,
08 90 4c 00 14 00 00 00 10 90 4c 00 14 90 4c 00
3c fb 7f 00 cc cc cc cc cc cc cc cc 00 90 4c 00
联合体
占用内存为最大的类型的大小,同时只能使用一个类型的变量
union NOGI {
char *huahua;
struct Huahua huahua_;
};
枚举
本质上就是整数,从0开始枚举,c语言中不限制对enum类型变量直接赋值,但是c++, Java中有限制
enum BOOK {
A, B, C, D, E
};
enum BOOK {
A, B, C, D = 10, E
};
9.3 大端序和小端序
内存地址从左往右增加
01 02 03 04
大端序 0x01020304
小端序 0x04030201
大端序一般用于网络传输,小端序一般用于CPU处理
判断大端和小端的方
int IsBigEnd() {
union {
short s;
char c[2];
} u = {.s = 0x0100};
return u.c[0] == 1;
}
int IsBigEnd2() {
short s = 0x0100;
char*p = (char *)&s;
return p[0] == 1;
}
10. 字符串
判断是否是某个范围的字符
#include "ctype.h"
//内部使用查表方式,0表示false,大于0表示true
printf("is digit, %d", isdigit('a'));
字符串转数值类型
stdlib.h
atox('') //使用比较简单,如果字符不认识直接略过
strtoxx(char*, char**) //支持多次识别同一个字符串,功能更丰富
字符串长度和比较
#include "string.h"
char s1[] = "hello my";
char s2[] = "hello world";
printf("%d\n", strcmp(s2, s1));
printf("%d\n", strlen(s1));
查找字符和子串
strchr(s1, 'e'); //从前往后查
strrchr(s1, '2'); //从后往前查
strpbrk(s1, "eo"); //查找多个字符
strstr(s1, "lo"); //查找子串
字符串分割
strtok(s1, s2);
字符串拷贝和连接
strcat(s1, s2);
strcpy(s1 + strlen(s1), s2);
内存操作
memcpy(s2, s1, strlen(s1)); //和strcpy相比,多了一个size_t
memmove(s2, s1, size_t); //在有内存重叠的时候使用
memset(dst, value, size); //设置内存的值
memchar() //和strchar一样,多了size
memcmp() //和strcmp一样,多了size
c99之后增加了restrict修饰符,表示多个参数之间内存不会重叠
c11之后增加了_s后缀的安全函数版本,返回一个错误码
11. 时间
11. 1 时间标准
UTC 世界调和时间,国际时间的标准
GMT 格林威治时间,0时区的时间
Epoch unix epoch,从1970年1月1日0时0分0秒开始的整数值
11.2 时间类型
time_t //从epoch开始的时间戳
clock_t //进程消耗的时间值
struct tm //更详细的可读的时间
{
int tm_sec; // seconds after the minute - [0, 60] including leap second
int tm_min; // minutes after the hour - [0, 59]
int tm_hour; // hours since midnight - [0, 23]
int tm_mday; // day of the month - [1, 31]
int tm_mon; // months since January - [0, 11]
int tm_year; // years since 1900
int tm_wday; // days since Sunday - [0, 6]
int tm_yday; // days since January 1 - [0, 365]
int tm_isdst; // daylight savings time flag
};
//精确到纳秒 //c11 msvc支持
struct timespec
{
time_t tv_sec; // Seconds - >= 0
long tv_nsec; // Nanoseconds - [0, 999999999]
};
//unix上支持,到毫秒
struct timeb;
struct timeval;
11.3 获取时间戳
//在不同平台上支持不同的时间类型,获取方式也不同,根据系统类型进行判断调用响应的函数
#if defined(_WIN32)
struct timeb time_buffer;
ftime(&time_buffer);
#elif defined(__unix__) || defined(__APPLE__)
struct timeval time_value;
gettimeofday(&time_value, NULL);
#elif defined(__STDC__) && __STDC_VERSION == 201102L
struct timespc timespec_value;
timespec_get(×pec_value, TIME_UTC);
#else
time_t current_time = time(NULL); //秒
#endif
11.4获取日历时间,和机器所处时区有关
time_t current_time;
time(¤t_time);
struct tm *current_tm = localtime(¤t_time);
printf("%d\n", current_tm->tm_min);
current_tm->tm_sec = 90;
mktime(current_tm); //能自动调整日历时间
printf("%d\n", current_tm->tm_min);
gmtime(¤t_time); //以gmt为标准计算
11.5 格式化时间到字符串
time_t current_time;
struct tm *current_tm;
//使用共享内存,格式 Thu Apr 22 13:02:50 2021
puts(asctime(current_tm));
puts(ctime(¤t_time));
//自定义格式
char current_time_s[20];
strftime(current_time_s, 20, "%Y-%m-%d %H:%M:%S", current_tm);
11.6 解析时间
//在unix系统上可以从字符串解析到tm类型
strptime(current_time_s,"%F %T", ¤t_tm);
11.7 计算时间差
//秒级
difftime(time_t, time_t);
//处理器时间
clock_t start_time_c = clock();
//do work
clock_t end_time_c = clock();
double cost = (end_time_c - start_time_c) * 1.0 / CLOCKS_PER_SEC;
12. 输入输出流
12.1 打开文件流
#include <stdio.h>
#include <errno.h>
FILE *file = fopen("CMakeLists.txt", "r");
if(file) {
puts("open successfully");
int err = ferror(file); //是否有错误
printf("%d\n", err);
int eof = feof(file); //是否到结尾
printf("%d\n", eof);
fclose(file); //记得关闭
} else {
printf("%d\n", errno); //打印错误码
perror("fopen"); //输出错误信息
}
12.2 文件流缓冲
#define BUFSIZ 8196
char fbuf[BUFSIZ]; //缓冲区要保证生命周期比文件流长
setvbuf(file, fbuf, _IOLBF, BUFSIZ); //给文件流指定缓冲区
fflush(stdout); //刷新缓冲区
按字符读写文件流,循环起来可以用来复制文件
//读取getchar() getc(file) fgetc(file), EOF表示结束
//写 putchar(int) putc(int, file) fputc(int, file)
FILE *file = fopen("CMakeLists.txt", "r");
if(file) {
setvbuf(file, fbuf, _IOLBF, BUFSIZ);
int next_c = getc(file);
while (next_c != EOF) {
putchar(next_c);
next_c = getc(file);
}
fclose(file);
} else {
printf("%d\n", errno);
perror("fopen");
}
12.3 按行读写文件(适用于文本)
gets(buffer); //从stdin读取,直到换行符,c11废除
gets_s(buffer, size); //指定读取的大小,安全
fgets(buffer, size, stdin); //指定输入文件流
puts(buffer)
fputs(buffer, size, stdout); //指定输出目的
按行复制文件,读取文件时会加锁解锁,所以按字符读取性能会较差
#include <stdio.h>
#include <locale.h>
#include <errno.h>
#define BUFSIZ 1024
char fbuf[BUFSIZ];
int main() {
setlocale(LC_ALL, "zh_CN.utf-8");
FILE *dstFile = fopen("newMakeList.txt", "w");
if (!dstFile) {
perror("dst file");
return errno;
}
FILE *file = fopen("CMakeLists.txt", "r");
if (!file) {
perror("src file");
return errno;
}
char *next = fgets(fbuf, 1024, file);
while (next) {
fputs(next, dstFile);
next = fgets(fbuf, 1024, file);
}
fclose(file);
fclose(dstFile);
return 0;
}
12.4 读写指定大小字节
//fread fwrite
void Echo() {
#define BUFFER_SIZE 4
char buffer[BUFFER_SIZE];
while (1) {
size_t bytes_read = fread(buffer, sizeof(buffer[0]), BUFFER_SIZE, stdin);
if (bytes_read < BUFFER_SIZE) {
if (feof(stdin)) {
puts("EOF");
fwrite(buffer, sizeof(buffer[0]), bytes_read, stdout);
} else if (ferror(stdin)) {
perror("stdin");
}
break;
}
fwrite(buffer, sizeof(buffer[0]), BUFFER_SIZE, stdout);
}
}
12.5 格式化输入输出
scanf(format, ...); //读取stdin
fscanf(file, format, ...); //读取文件流
sscanf(buffer, format, ...); //读取char
printf(format, ...); //写到stdout
fprintf(file, format, ...); //写到文件流
sprintf(buffer, format, ...); //写到buffer
snprintf(buffer, size, format, ...); //指定大小
12.6 重定向
freopen(filename, mode, stream); //把文件流重定向到一个文件,把原来的文件描述符指向了新的文件
dup(fildes); //新建一个fd
dup2(fildes, fildes2); //把fd2指向fd1
fileno(file); //获得文件描述符
close(fd); //关闭fd
12.7 流的位置
ftell(file); //返回当前流位置
fseek(file, offset, SEEK_CUR); //定位到某个位置
其他操作,太多了
remove(path); //删除文件
rename(name1, name2); //重命名文件
13. 线程
c标准库直到c11才提供了线程支持。
13.1 线程基本操作
#include <tinycthread.h>
#include <stdio.h>
int SayHello(char *name) {
printf("run in new thread[%#x]: hello, %s", thrd_current(), name);
return 0;
}
int main() {
thrd_t newThread;
int result = thrd_create(&newThread, SayHello, "huahua"); //创建线程,获得结果
if (result == thrd_success) {
printf("good");
} else {
printf("bad");
}
thrd_join(newThread, &result); //子线程先执行
// thrd_yield(); //让出当前线程CPU
// thrd_sleep(&(struct timespec){.tv_sec = 1}, NULL); //线程休眠1秒
// thrd_detach(newThread); //线程结束自动回收
}
13.2 线程安全问题
对共享资源进行非原子的并发访问
不同线程之间代码的可见性
线程内部代码编译的重排序问题
int num;
int Counter() {
for (int i = 0; i < 100000; ++i) {
num++;
}
return 0;
}
int main() {
thrd_t t1;
thrd_t t2;
thrd_create(&t1, Counter, NULL);
thrd_create(&t2, Counter, NULL);
thrd_join(t1, NULL);
thrd_join(t2, NULL);
printf("%d", num);
}
volatile
c语言中的目的是禁止编译器优化读写操作
不会保证原子性,根据编译器的不同不一定支持可见性(MSVC支持),和Java中的volatile不同
volatile int num;
原子类型
#include <stdatomic.h>
//使用atomic前缀声明原子类型
atomic_int num = 0;
atomic_flag flag; //适合用作原子标志位
atomic_flag_test_and_set(&flag);
atomic_flag_clear(&flag);
锁,性能相对较差
int num = 0;
mtx_t mutex;
int Counter() {
for (int i = 0; i < 1000000; ++i) {
mtx_lock(&mutex);
num++;
mtx_unlock(&mutex);
}
return 0;
}
int main() {
mtx_init(&mutex, mtx_plain); //初始化锁
thrd_t t1;
thrd_t t2;
thrd_create(&t1, Counter, NULL); //创建线程
thrd_create(&t2, Counter, NULL); //创建线程
thrd_join(t1, NULL);
thrd_join(t2, NULL);
mtx_destroy(&mutex); //释放锁
printf("%d", num);
}
threadlocal
//声明变量为_Thread_local 变量在每个线程有一个副本
//tss 在线程内部创建内存,避免多线程问题
_Thread_local int num = 0;
副作用,只有局部变量操作,不对外部产生影响
//如果函数只产生返回值,不访问外部资源,不产生副作用,就不会导致线程安全的问题
int Counter() {
int local_num = 0; //局部变量
for (int i = 0; i < 1000000; ++i) {
local_num++;
}
return local_num;
}
int main() {
thrd_t t1;
thrd_t t2;
thrd_create(&t1, Counter, NULL);
thrd_create(&t2, Counter, NULL);
int count = 0;
int result;
thrd_join(t1, &result);
count += result;
thrd_join(t2, &result);
count += result;
printf("%d", count);
}
13.3 实现线程返回复杂结果
thrd_join(t1, &result); //传入参数只支持int类型。如果要返回复杂结果,要通过入参。
thrd_create(&t1, Counter, void*); //创建线程时支持的是void*,所以可以传入复杂结构到函数中,在函数中实现修改
//Counter(void*)
//类似的在一般函数中要返回复杂结果,也是在参数列表中传入结果指针
回调
//通过传入一个函数指针,函数完成时调用回调函数
struct counter {
int result;
void (*callback)(struct counter* ctr); //callback是个指针,指向函数,参数为counter,返回为void
};
void CounterCallBack(struct counter *ctr) {
printf("finished, result is %d\n", ctr->result);
}
int Counter(struct counter *ctr) {
int local_num = 0;
for (int i = 0; i < 1000000; ++i) {
local_num++;
}
ctr->result = local_num;
ctr->callback(ctr);
return 0;
}
int main() {
struct counter c = (struct counter) {
.result = 0,
.callback = CounterCallBack
};
thrd_t t1;
thrd_t t2;
thrd_create(&t1, Counter, &c);
thrd_create(&t2, Counter, &c);
thrd_join(t1, NULL);
thrd_join(t2, NULL);
}