c语言学习简要笔记

大学的时候学习过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选择自己安装的路径。
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) = &Sum;
 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(&timespec_value, TIME_UTC);
#else
  time_t current_time = time(NULL);  //秒
#endif

11.4获取日历时间,和机器所处时区有关

  time_t current_time;
  time(&current_time);
  struct tm *current_tm = localtime(&current_time);
  printf("%d\n", current_tm->tm_min);
  current_tm->tm_sec = 90;
  mktime(current_tm);  //能自动调整日历时间
  printf("%d\n", current_tm->tm_min);
  gmtime(&current_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(&current_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", &current_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);
}
  • 6
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值