C语言技术概要
一、基本数据类型及基础知识
1.关键字
1.数据类型相关
char 1字节、short 2字节、int 4字节、long 32位 4字节 64位8字节、 float 4字节、 double 8字节、
struct、union、enum、signed、unsigned、void
我们在代码中打印数据类型所占字节数:
char a;
short b;
int c;
long d;
float e;
double f;
printf("char %d short %d int %d long %d float %d double %d\n",sizeof(a),sizeof(b),sizeof(c),sizeof(d)
,sizeof(e),sizeof(f));
2.存储类型相关
- 1、register是寄存器的意思,用register修饰的变量是寄存器变量,
即:在编译的时候告诉编译器这个变量是寄存器变量,尽量将其存储空间分配在寄存器中。
注意:
(1):定义的变量不一定真的存放在寄存器中。
(2):cpu取数据的时候去寄存器中拿数据比去内存中拿数据要快
(3):因为寄存器比较宝贵,所以不能定义寄存器数组
(4):register只能修饰字符型及整型的,不能修饰浮点型 - 2.static可以修饰全局变量、局部变量、函数
- 3.const 是常量的意思用const修饰的变量是只读的,不能修改它的值
- 4.auto int a;和int a是等价的,auto关键字现在基本不用
- 5.extern 是外部的意思,一般用于函数和全局变量的声明,
3.条件控制相关
- 条件控制语句:
if语句:if else
switch语句:switch case default - 循环控制语句:
for while do goto - 辅助控制语句:
break continue
4.其他关键字
- sizeof 求所占内存大小、
- typedef 取别名、
- volatile 线程同步相关用volatile定义的变量,是易改变的,即告诉 cpu每次用 volatile变量的时候,重新去内存中取
保证用的是最新的值,而不是寄存器中的备份。
2.数据类型
1.基本类型
char、short int 、int、long int、float 小数点 后6位、double小数点后6位
2.构造类型
数组、结构体、共用体、枚举
3.常量与变量
常量不可改变
常量:在程序运行过程中,其值不可以改变的量
例:100u2018a'“hello”
整型100,125,-100,0
实型3.14,0.125f,-3.789
字符型'a',b',2'
字符串“a”,“ab”,“1232”
字符类型的都是由ASCII码存储的
变量可修改
4.全局与局部
1.外部函数
声明放在头文件,实现在.c,引用头文件即可调用
咱们定义的普通函数,都是外部函数。
即函数可以在程序的任何一个文件中调用。
3.7.7内部函数
在定义函数的时候,返回值前面加static 修饰。这样的函数只能在当前.c内部使用,即使你包含了该头文件
扩展:
在同一作用范围内,不允许变量重名。
作用范围不同的可以重名。
局部范围内,重名的全局变量不起作用。(就近原则)
I
int num=100;/全局变量
int main()
/如果出现可以重名的情况,使用的时候满足向上就近原则
int num=999;/局部变量
return 0;
5.格式化输出
格式化输出字符:
%d十进制有符号整数
%ld十进制long有符号整数
%u十进制无符号整数
%o以八进制表示的整数
%x以十六进制表示的整数
%ffloat型浮点数
%lfdouble型浮点数
%e
指数形式的浮点数
%c
特殊应用:
%3d
%03d
%-3d
%5.2f
%3d
要求宽度为3位,如果不足3位,前面空格补齐;如果足够3位,此语句无效
%03d
要求宽度为3位,如果不足3位,前面0补齐;如果足够3位,此语句无效
%-3d
要求宽度为3位,如果不足3位,后面空格补齐;如果足够3位,此语句无效
%.2f
小数点后只保留2位
.
单个字符
%s
字符串
%p
指针的值
6.类型转换
- 强制转换
- 自动转换
占用内存字节数少(值域小)的类型,向占用内存字节数多(值域大)的类型转换,以保证精度不降低
7.goto循环的使用
printf("1111111\n");
goto NEXT; //goto进行跳转
printf("222222\n");
NEXT: printf("333333\n");
int b=1;
int sum=0;
LOOP:
b++;
sum+=b;
if(b<10){
goto LOOP;//goto实现循环
}
printf("sum=%d\n",sum);
8.数组的一些基本概念
- 1.数组是存储相同类型
- 2.内存中连续有序存储
- 3.数组名即数组首地址
- 4.数组名是常量并非变量
9.字符数组
- 1.char [] 以\0结尾
- 2.字符数组 char[]本身是可以被修改的。你可以直接通过数组名和下标访问数组中的元素,并进行修改(除非数组被声明为
const) - 3.注意char* 指向的常量区不可被修改,但是其可以指向其他区域。 char* str = "hello"; str[0] = 'H'; // 错误!尝试修改字符串常量的内容
存储方式
-
char *:char *是一个指向字符的指针。它本身并不存储字符数据,而是存储一个内存地址,该地址指向字符数据的存储位置。- 这个内存地址可以是静态分配的(如指向字符串常量),也可以是动态分配的(如通过
malloc或calloc函数)。 - 当
char *指向一个字符串常量时,字符串常量通常存储在程序的只读数据段(如字符串字面量区或文本段)中。
-
char[]:char[]是一个字符数组。它在栈上分配一段连续的存储空间来存储字符数据。- 数组的大小在声明时确定,并且不能改变。
- 数组名通常被当作指向其第一个元素的指针来使用,但这个指针是隐式的,你不能直接改变数组名所指向的地址。
生命周期
-
char *:- 如果
char *指向的是一个字符串常量,那么这个字符串常量的生命周期与程序的整个生命周期相同。 - 如果
char *指向的是动态分配的内存(通过malloc或calloc),那么这块内存的生命周期取决于你何时使用free函数来释放它。如果不释放,将会导致内存泄漏。
- 如果
-
char[]:char[]数组的生命周期通常与包含它的变量或对象相同。当变量或对象离开其作用域时,数组所占用的内存空间会自动被释放。
可修改性
-
char *:- 如果
char *指向的是一个可写的内存区域(如动态分配的内存或字符数组),那么你可以通过指针来修改该区域中的字符数据。 - 但是,如果
char *指向的是一个字符串常量,尝试修改其内容将会导致未定义的行为(通常是程序崩溃),因为字符串常量通常存储在程序的只读数据段中。
- 如果
-
char[]:char[]数组的内容是可以被修改的。你可以直接通过数组名和下标来访问和修改数组中的字符数据。
示例
-
使用
char *指向字符串常量:char *str = "hello"; // str 指向一个字符串常量 // str[0] = 'H'; // 错误!尝试修改字符串常量的内容 -
使用
char *动态分配内存:char *dynStr = malloc(6 * sizeof(char)); // 分配6个字节的内存 if (dynStr != NULL) { strcpy(dynStr, "hello"); // 复制字符串到动态分配的内存中 dynStr[0] = 'H'; // 可以修改动态分配的内存中的内容 free(dynStr); // 释放动态分配的内存 } -
使用
char[]声明数组:char arr[] = "hello"; // arr 是一个字符数组 arr[0] = 'H'; // 可以修改数组中的内容 // 数组arr的生命周期与包含它的变量或对象的生命周期相同
10.函数声明
- 1.头文件声明
- 2.在main函数前声明
11.外部函数与内部函数
1.外部函数
声明放在头文件,实现在.c,引用头文件即可调用
咱们定义的普通函数,都是外部函数。
即函数可以在程序的任何一个文件中调用。
3.7.7内部函数
在定义函数的时候,返回值前面加static 修饰。这样的函数只能在当前.c内部使用,即使你包含了该头文件
扩展:
在同一作用范围内,不允许变量重名。
作用范围不同的可以重名。
局部范围内,重名的全局变量不起作用。(就近原则)
I
int num=100;/全局变量
int main()
/如果出现可以重名的情况,使用的时候满足向上就近原则
int num=999;/局部变量
return 0;
12.define、include与编译过程
1.c语言编译过程
1、预处理 gcc -E hello.c-o hello.i
2、编译gcc -S hello.i -o hello.s
3、汇编 gcc -C hello.s -o hello.o
4、链接 gcc hello.o -o hello_elf
1:预编译
将.c中的头文件展开、宏展开
生成的文件是.i文件
2:编译
将预处理之后的.i文件生成.s汇编文件
3、汇编
将.s汇编文件生成.o目标文件
4、链接
将.0文件链接成目标文件
2.include
包含头文件 <>系统指定的 ”“ 现在本地在找系统,尽量不要包含.c因为预处理会被展开
3.define
1.宏替换 预处理的时候的替换
2、带参宏
#define S(a,b) ab
注意带参宏的形参a和b没有类型名,
S(2,4)将来在预处理的时候替换成实参替代字符串的形参,其他字符保留,24
//带参宏
//带参宏类似于一个简单的函数,将函数的参数进行设置,就可以传递给对应的表达式
//#define S(a, b) a*b
int main(int argc, char *argv[])
{
printf("PI = %lf\n", PI);
double d = PI;
printf("d = %lf\n",d);
printf("%d\n",S(2,4));
//注意:宏定义只是简单的替换,不会自动加括号
//带参宏1:2+8*4=34
//带参宏2:((2+8)*(4))=40
printf("%d\n",S(2+8,4));
return ;
}
带参宏被调用多少次就会展开多少次,执行代码的时候没有函数调用的过程,不需要压栈弹栈。所以带参宏,
是浪费了空间,因为被展开多次,节省时间。
带参函数,代码只有一份,存在代码段,调用的时候去代码段取指令,调用的时候要,压栈弹栈。有个调用
的过程。
所以说,带参函数是浪费了时间,节省了空间。
带参函数的形参是有类型的,带参宏的形参没有类型名。
3.选择性编译
选择性编译
1、
#ifdef
AAA
代码段一
#else
代码段二
#endif
如果在当前.qifdef上边定义过AAA,就编译代码段一,否则编译代码段二
注意和ifelse语句的区别,ifelse 语句都会被编译,通过条件选择性执行代码
而选择性编译,只有一块代码被编译
2.
#ifndef
AAA
代码段
#else
代码段
#endif
和第一种互补。
这种方法,经常用在防止头文件重复包含。
I
防止头文件重复包含:常用于多文件编程中.h的第一行就是#ifndef
3、
#if表达式
程序段一
#else
程序段二
#endif
如果表达式为真,编译第一段代码,否则编译第二段代码
这种形式一般用于注释多行代码
选择性编译都是在预编译阶段干的事情。
二、指针的概念
1. 指针的定义
- 指针的本质:指针是一个变量,用来存放内存地址的变量,我们通常称为指针变量。
- 指针的作用:通过指针,可以访问内存中的数据,实现直接访问和操作内存中的数据,如动态内存分配、数组操作等。
2. 指针的大小
- 指针的大小是由计算机的物理性质决定的。
- 在32位机器上,地址是由32根地址线组成,所以一个指针变量的大小为4个字节(32位/8位 = 4字节)。
- 在64位机器上,地址是由64根地址线组成,所以一个指针变量的大小为8个字节(64位/8位 = 8字节)。
3. 指针的类型
- 数据的存储方式有多种,如char、int、float等,因此对应的指针类型也有多种,如char*、int*、float*等。
- 指针的类型决定了对指针解引用时访问数据的大小(权限)。例如,char的指针解引用只能访问一个字节,而int的指针解引用能访问四个字节(这取决于int类型在特定编译器和机器上的大小)。
4. 指针的用法
-
定义指针:通过在变量名前加“*”号来定义指针,如
int *p;。 -
初始化指针:指针变量定义后需要初始化,否则可能指向一个随机地址,导致程序出错。可以通过“&”符号获取变量的地址来初始化指针,或者直接将指针初始化为NULL。
-
操作指针:
- 取地址操作:使用“&”符号获取变量的地址。
- 解引用操作:使用“*”符号访问指针指向地址中的数据。
- 指针运算:指针变量可以进行加减运算(移动指针位置),也可以进行自增自减运算。指针相减可以得到两指针之间数据的个数(常用于数组处理)。
-
指针和数组:数组名本身就是一个指向数组首元素的指针。指针变量可以像数组一样访问数组中的元素。
-
指针和函数:指针在函数中的应用非常广泛,可以用来实现函数的参数传递、返回值传递等。
5. 指针的步长
- 指针的步长(增量)取决于指针所指向的数据类型。对于不同类型的指针,进行算术运算时移动的字节数是不同的。
6. 指针转换
- 指针类型转换允许改变指针变量所指向的数据类型。这可以通过隐式转换(如void*的自动转换)或显式转换(使用类型转换操作符)来实现。
- 不恰当的指针类型转换可能导致程序出现未定义的行为或崩溃,因此应谨慎使用
7.指针安全性
在C和C++等语言中,指针操作可能导致内存泄漏或悬挂指针(也称为野指针或悬空指针),这些问题通常会导致程序崩溃、数据损坏或安全漏洞。以下是一些避免这些问题的最佳实践:
1. 初始化指针
确保在使用指针之前,总是将其初始化为一个有效的地址或nullptr(或NULL,但在C++11及更高版本中,推荐使用nullptr)。这可以防止指针指向未定义的内存位置。
int* ptr = nullptr;
2. 避免野指针
在释放内存后,立即将指针设置为nullptr,以防止成为野指针。
delete ptr;
ptr = nullptr;
对于数组,使用delete[]来释放内存。
delete[] ptr;
ptr = nullptr;
3. 检查指针是否为空
在解引用指针之前,总是检查它是否为nullptr。
if (ptr != nullptr) {
// 使用ptr
}
4. 避免多重释放
确保不要多次释放同一个内存块。在释放后,将指针设置为nullptr可以帮助防止这种情况。
5. 使用智能指针(C++特有)
C++提供了几种智能指针类型(如std::unique_ptr、std::shared_ptr和std::weak_ptr),它们可以自动管理内存生命周期,减少内存泄漏和悬挂指针的风险。
6. 使用RAII(Resource Acquisition Is Initialization)
RAII是一种在C++中管理资源的策略,它确保在对象的生命周期内自动管理资源(如内存、文件句柄等)。通过将资源封装在对象中,并在对象的构造函数中获取资源,在析构函数中释放资源,可以确保资源在不再需要时得到释放。
7. 避免裸指针传递所有权
在函数之间传递指针时,要明确所有权和生命周期。如果一个函数需要接管指针的所有权(即负责释放内存),则应该使用智能指针或明确约定。
8. 使用内存检查工具
使用如Valgrind、AddressSanitizer(ASan)等内存检查工具可以帮助识别内存泄漏和悬挂指针。
9. 遵守编程规范
遵循良好的编程规范,如保持代码简洁、避免复杂的指针操作、减少全局变量和动态内存分配等,可以降低出现问题的风险。
10. 测试和审查代码
编写测试用例来验证指针操作的正确性,并对代码进行定期审查,以确保没有引入新的内存管理问题
三、指针数组与数组指针
指针数组(Array of Pointers)和数组指针(Pointer to Array)是两个容易混淆的概念,但它们在用途和声明方式上有显著的区别。
指针数组(Array of Pointers)
定义:指针数组是一个数组,其每个元素都是一个指针。
声明方式:
type *name[size];
这里的 type 是指针所指向的类型,name 是数组的名称,size 是数组的大小。
示例:
char *names[] = {"Alice", "Bob", "Charlie"};
在这个例子中,names 是一个指针数组,它包含了三个字符指针,分别指向三个字符串常量。
用途:指针数组常用于存储指向多个相同或不同类型对象的指针,比如字符串数组、对象数组等。
数组指针(Pointer to Array)
定义:数组指针是一个指针,它指向一个数组的首个元素。更具体地说,它指向一个具有固定大小(至少第一个维度的大小)的数组。
声明方式:
type (*name)[size];
这里的 type 是数组元素的类型,name 是指针的名称,size 是数组的大小(通常是数组的第一个维度的大小)。
示例:
int arr[3] = {1, 2, 3};
int (*arr_ptr)[3] = &arr;
在这个例子中,arr_ptr 是一个数组指针,它指向一个包含三个整数的数组。
用途:数组指针通常用于处理多维数组,因为它允许我们直接指向一个完整的数组(或数组的行),而不是单独的元素。在遍历二维数组时,数组指针特别有用。
区别
- 声明方式:指针数组的声明中,
[]在*后面,表示一个数组的元素是指针。数组指针的声明中,*在()中,并且[]在()外面,表示这是一个指向数组的指针。 - 用途:指针数组通常用于存储多个指向不同类型或相同类型对象的指针。而数组指针主要用于指向数组,特别是多维数组的行。
- 内存布局:指针数组在内存中存储的是多个指针的值(即地址),而数组指针在内存中存储的是一个数组的首地址。
- 访问元素:通过指针数组访问元素时,需要首先解引用指针(即使用
*运算符),然后再使用下标访问元素。而通过数组指针访问元素时,可以直接使用下标访问,因为数组指针已经指向了数组的首个元素。
总结
指针数组和数组指针在声明方式、用途和内存布局上都有显著的区别。指针数组是一个存储多个指针的数组,而数组指针是一个指向数组的指针。在使用时,我们需要根据具体的需求选择正确的类型。
四、指针函数与函数指针
指针函数(Pointer to Function)和函数指针(Function Pointer)在C语言中是两个不同但容易混淆的概念。这里我们将分别解释它们。
指针函数
指针函数实际上是指返回一个指针的函数,而不是指向函数的指针。这种函数的返回类型是一个指针。
例如,一个函数可能返回一个指向整数的指针:
int* getArrayPointer() {
static int arr[] = {1, 2, 3, 4, 5};
return arr; // 返回一个指向整数的指针
}
在这个例子中,getArrayPointer 是一个指针函数,因为它返回一个 int* 类型的值(即指向整数的指针)。
函数指针
函数指针是一个变量,它的值是一个函数的地址。你可以通过这个函数指针来调用这个函数。
首先,你需要定义一个函数指针类型。然后,你可以使用这个类型来声明函数指针变量,并将函数的地址赋值给它。
例如:
// 定义一个函数类型
typedef int (*FunctionPointerType)(int, int);
// 声明一个接受两个int参数并返回int的函数
int add(int a, int b) {
return a + b;
}
// 主函数中
int main() {
// 声明一个函数指针变量
FunctionPointerType funcPtr;
// 将add函数的地址赋值给函数指针
funcPtr = add;
// 通过函数指针调用函数
int result = funcPtr(2, 3);
printf("Result: %d\n", result); // 输出: Result: 5
return 0;
}
在这个例子中,FunctionPointerType 是一个函数指针类型,它指向一个接受两个 int 参数并返回 int 的函数。然后,我们声明了一个 FunctionPointerType 类型的变量 funcPtr,并将 add 函数的地址赋值给它。最后,我们通过 funcPtr 调用了 add 函数。
总结
- 指针函数:是指返回一个指针的函数。
- 函数指针:是一个变量,它的值是一个函数的地址,你可以通过这个函数指针来调用这个函数。
两者在命名上可能有些误导,但关键是理解它们分别是什么:一个是返回指针的函数,另一个是指向函数的指针。
五、常量指针与指针常量
在C语言中,常量指针(Pointer to Constant)和指针常量(Constant Pointer)是两个不同的概念,它们的命名方式可能有些混淆,但通过仔细分析可以明确区分它们。
常量指针(Pointer to Constant)
常量指针是指一个指针,它指向的对象是常量,即指针所指向的内容不能被修改。但是,这个指针本身(即它所存储的地址)是可以被修改的。
常量指针的声明方式通常如下:
const char *ptr; // ptr 是一个指向字符常量的指针
在这个例子中,ptr 是一个指针,它指向了一个字符常量。你不能通过 ptr 来修改它所指向的字符(除非这个字符原本就不是常量)。但是,你可以改变 ptr 指向的地址。
指针常量(Constant Pointer)
指针常量是指一个指针,它的值(即它所存储的地址)在初始化之后不能被修改。但是,这个指针所指向的内容(如果内容不是常量)是可以被修改的。
指针常量的声明方式通常如下:
char *const ptr; // ptr 是一个常量指针
在这个例子中,ptr 是一个指针常量,它的值(即它所指向的地址)在初始化之后不能被修改。但是,你可以通过 ptr 来修改它所指向的内容(如果内容不是常量的话)。
示例
常量指针
const char *str = "Hello, World!"; // str 是一个指向字符常量的指针
// str[0] = 'h'; // 错误!不能通过str修改字符串常量的内容
const char *anotherStr = "Another string"; // 可以改变str指向的地址
指针常量
char message[] = "Hello, World!";
char *const ptr = message; // ptr 是一个常量指针,指向message数组的首地址
ptr[0] = 'h'; // 正确!可以通过ptr修改message数组的内容
// ptr = anotherStr; // 错误!不能改变ptr的值(即它所指向的地址)
总结
- 常量指针:指向常量的指针,即指针指向的内容是常量,但指针本身的值(即它所存储的地址)可以改变。
- 指针常量:指针本身是常量,即指针的值(即它所存储的地址)在初始化之后不能被修改,但指针指向的内容(如果内容不是常量)可以被修改。
六、结构体、联合体
结构体(Structure)和联合体(Union)是C和C++编程语言中用于组织数据的两种结构,但它们在使用和内存布局上有显著的不同。
结构体(Structure)
结构体是一种用户自定义的数据类型,它允许你将不同类型的数据项(基础数据类型或其他结构体)组合成一个单独的类型。结构体中的每个数据项称为成员(member),结构体变量可以包含多个成员。结构体在内存中占用的是所有成员所需的内存的总和,并且每个成员都拥有独立的内存空间。
示例:
struct Person {
char name[50];
int age;
float height;
};
int main() {
struct Person alice;
strcpy(alice.name, "Alice");
alice.age = 30;
alice.height = 1.65f;
// ...
return 0;
}
在上面的示例中,我们定义了一个Person结构体,它包含三个成员:name(一个字符数组)、age(一个整数)和height(一个浮点数)。然后我们创建了一个Person类型的变量alice,并给它的成员赋值。
结构体与结构体指针
结构体(struct)和结构体指针在C++(以及C语言)中有明显的区别。以下是它们之间的主要差异:
结构体(struct)
- 定义:结构体是一种用户自定义的数据类型,用于组合多个不同类型的数据项(成员)到一个单独的类型中。
- 内存分配:当定义一个结构体变量时,编译器会在栈上或静态存储区(取决于变量的声明位置)为该变量分配足够的内存来存储其所有成员。
- 直接访问:可以通过
.运算符(成员访问运算符)直接访问结构体的成员。 - 传递:作为参数传递给函数时,结构体通常是通过值传递的,这意味着会创建一个结构体的副本。
- 生命周期:结构体变量的生命周期与其定义的作用域相同。
结构体指针(struct *)
- 定义:结构体指针是一个变量,其值为另一个变量的地址,该变量是某种结构体类型的实例。
- 内存分配:结构体指针本身只占用足够的内存来存储地址(在32位系统上通常是4个字节,在64位系统上通常是8个字节)。而它指向的结构体实例的内存分配则取决于该实例是在哪里以及如何被创建的。
- 间接访问:需要通过
->运算符(指针成员访问运算符)来访问结构体指针所指向的结构体的成员。 - 传递:作为参数传递给函数时,结构体指针通常是通过引用传递的,这意味着函数直接操作原始数据,而不是数据的副本。
- 生命周期:结构体指针的生命周期与其定义的作用域相同,但它所指向的结构体实例的生命周期可能更长或更短。
- 动态内存分配:结构体指针经常与动态内存分配一起使用(如使用
new和delete运算符),以便在运行时创建和销毁结构体实例。
示例
struct Point {
int x;
int y;
};
int main() {
// 定义一个结构体变量
Point p1;
p1.x = 10;
p1.y = 20;
// 定义一个结构体指针并分配内存
Point* p2 = new Point();
p2->x = 30;
p2->y = 40;
// ... 使用p1和p2 ...
// 释放动态分配的内存
delete p2;
return 0;
}
在这个示例中,p1是一个结构体变量,而p2是一个指向结构体实例的指针。它们都可以用来存储和操作Point类型的数据,但它们在内存管理和访问方式上有所不同。
联合体(Union)
联合体也是一种用户自定义的数据类型,但与结构体不同的是,联合体的所有成员共享同一块内存空间。这意味着在联合体的某个时间点,只有一个成员是有效的,因为它会覆盖其他所有成员的内存。联合体的总大小等于其最大成员的大小。
示例:
union Data {
int i;
float f;
char str[4]; // 假设在平台上int和float都不小于4字节
};
int main() {
union Data data;
data.i = 123; // 此时,data的内存中存储的是整数123的二进制表示
// 如果现在访问data.f,可能会得到一个无效或不可预测的浮点数
// ...
return 0;
}
在上面的示例中,我们定义了一个Data联合体,它包含三个成员:i(一个整数)、f(一个浮点数)和str(一个字符数组)。但是,由于联合体的特性,这三个成员不会同时存储数据。当我们给data.i赋值时,data.f和data.str的内容就会被覆盖。
联合体判断大小端
在C++中,联合体(union)本身并不直接用于判断系统的字节序(大端或小端)。但是,你可以利用联合体的特性,结合字符数组和整型来编写一个检测字节序的代码。
在大小端系统中,多字节数据的存储顺序是不同的。大端系统(Big-Endian)中,最高有效位字节(MSB)存储在最低的内存地址处,而小端系统(Little-Endian)中,最低有效位字节(LSB)存储在最低的内存地址处。
以下是一个使用联合体和字符数组来判断大小端的例子:
#include <iostream>
union EndianTest {
uint32_t value;
char bytes[4];
};
bool isLittleEndian() {
EndianTest test;
test.value = 0x01020304;
return test.bytes[0] == 0x04; // 如果最低地址处是0x04,则是小端
}
int main() {
if (isLittleEndian()) {
std::cout << "The system is little-endian." << std::endl;
} else {
std::cout << "The system is big-endian." << std::endl;
}
return 0;
}
在这个例子中,我们定义了一个联合体EndianTest,它包含一个uint32_t类型的value和一个字符数组bytes。我们设置value为0x01020304,然后检查bytes数组的第一个元素。如果它是0x04,那么系统就是小端的,因为最低有效位字节(LSB)存储在最低的内存地址处。否则,系统就是大端的
总结
- 结构体(Structure)用于将多个不同类型的数据组合成一个单独的类型,每个成员都有自己的内存空间。
- 联合体(Union)也用于组合多个不同类型的数据,但所有成员共享同一块内存空间,同一时间只有一个成员是有效的。
七、文件操作与线程操作
文件操作
在C语言中,文件操作是通过标准库中的文件操作函数来实现的。以下是一些基本的文件操作函数和它们的用途:
- 打开文件:
FILE *fopen(const char *filename, const char *mode);
这个函数用于打开一个文件,并返回一个指向FILE结构的指针,这个结构包含了文件的信息。如果文件不能被打开,它将返回NULL。
mode参数决定了文件的打开方式,例如:
* `"r"`:只读模式
* `"w"`:写模式(如果文件已存在,则覆盖它;如果文件不存在,则创建它)
* `"a"`:追加模式(如果文件已存在,则数据被写入到文件的末尾;如果文件不存在,则创建它)
* `"r+"`:读写模式(从文件头开始)
* `"w+"`:读写模式(如果文件已存在,则覆盖它;如果文件不存在,则创建它)
* `"a+"`:读写模式(如果文件已存在,则数据被写入到文件的末尾;如果文件不存在,则创建它)
- 关闭文件:
int fclose(FILE *stream);
这个函数用于关闭一个已打开的文件。如果成功关闭文件,则返回0;否则返回EOF(一个预定义的常量,表示文件结束或出错)。
3. 读取文件:size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
这个函数用于从文件中读取数据。它读取nmemb个元素,每个元素的大小为size字节,并将它们存储在ptr所指向的数组中。返回实际读取的元素数量。
4. 写入文件:size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
这个函数用于将数据写入文件。它写入nmemb个元素,每个元素的大小为size字节,数据来自ptr所指向的数组。返回实际写入的元素数量。
5. 获取文件位置:int fgetpos(FILE *stream, fpos_t *pos); 和 int fsetpos(FILE *stream, const fpos_t *pos);
这两个函数用于获取和设置文件的位置指针。fgetpos将当前的文件位置存储在pos所指向的对象中,fsetpos则将文件位置设置为pos所指向的对象中的值。
6. 移动文件位置:int fseek(FILE *stream, long offset, int whence);
这个函数用于移动文件的位置指针。offset是相对于whence的偏移量。whence可以是SEEK_SET(从文件开始计算)、SEEK_CUR(从当前位置计算)或SEEK_END(从文件末尾计算)。
7. 检查文件结束和错误:int feof(FILE *stream); 和 int ferror(FILE *stream);
feof函数用于检查是否已到达文件末尾,如果到达则返回非零值;ferror函数用于检查在文件操作期间是否发生了错误,如果发生错误则返回非零值。
请注意,以上所有函数都应在包含stdio.h头文件的情况下使用。此外,C语言中的文件操作还有很多其他函数和特性,这里只列出了一些基本的。
8.以下是一个C语言文件读写的简单样例,该样例展示了如何打开文件、写入数据、读取数据以及关闭文件。
#include <stdio.h>
#include <stdlib.h>
int main() {
// 打开文件用于写入,如果文件不存在则创建它
FILE *fp = fopen("example.txt", "w");
if (fp == NULL) {
perror("Error opening file for writing");
exit(EXIT_FAILURE);
}
// 写入数据到文件
fprintf(fp, "Hello, World!\n");
fprintf(fp, "This is an example of file writing in C.\n");
// 关闭文件
if (fclose(fp) != 0) {
perror("Error closing file");
exit(EXIT_FAILURE);
}
// 重新打开文件用于读取
fp = fopen("example.txt", "r");
if (fp == NULL) {
perror("Error opening file for reading");
exit(EXIT_FAILURE);
}
// 读取并打印文件内容
char ch;
while ((ch = fgetc(fp)) != EOF) {
putchar(ch);
}
// 关闭文件
if (fclose(fp) != 0) {
perror("Error closing file");
exit(EXIT_FAILURE);
}
printf("\nFile reading and writing completed successfully.\n");
return 0;
}
这个样例首先尝试打开一个名为example.txt的文件用于写入,如果文件不存在,则会创建它。然后,它使用fprintf函数将两行文本写入文件。写入完成后,它关闭文件。
接下来,它重新打开同一个文件用于读取。使用fgetc函数逐个字符地读取文件内容,并使用putchar函数将读取到的字符打印到屏幕上。读取完成后,它再次关闭文件。
如果在打开、写入、读取或关闭文件的过程中发生错误,程序会打印出相应的错误信息,并使用exit(EXIT_FAILURE)退出程序。如果所有操作都成功完成,程序会打印一条消息表示文件读写操作已成功完成。
线程操作
在C语言中,线程(Thread)的创建和管理并不是由标准C库直接提供的,因为线程的概念与特定的操作系统紧密相关。然而,许多操作系统都提供了线程库,这些库可以在C语言中使用。
在POSIX兼容的系统中(如Linux和macOS),你可以使用POSIX线程(Pthreads)库来创建和管理线程。以下是一个简单的Pthreads示例,展示了如何在C语言中创建和运行一个线程:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
// 线程运行的函数
void *print_hello_world(void *threadid) {
long tid;
tid = (long)threadid;
printf("Hello World! It's me, thread #%ld!\n", tid);
pthread_exit(NULL);
}
int main(int argc, char *argv[]) {
pthread_t thread1, thread2;
int rc;
long t1 = 1, t2 = 2;
// 创建线程,独立运行函数 print_hello_world
rc = pthread_create(&thread1, NULL, print_hello_world, (void *)t1);
if (rc) {
printf("ERROR; return code from pthread_create() is %d\n", rc);
exit(-1);
}
rc = pthread_create(&thread2, NULL, print_hello_world, (void *)t2);
if (rc) {
printf("ERROR; return code from pthread_create() is %d\n", rc);
exit(-1);
}
// 等待线程完成
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
printf("Main thread exiting.\n");
pthread_exit(NULL);
}
在这个示例中,print_hello_world 函数是线程运行的函数。我们使用 pthread_create 函数创建了两个线程,并将 print_hello_world 函数作为它们的入口点。pthread_create 的第一个参数是一个指向 pthread_t 类型变量的指针,该变量用于存储新线程的ID。第二个参数是线程属性,这里我们设置为 NULL,表示使用默认属性。第三个参数是线程运行的函数,第四个参数是传递给线程函数的参数。
pthread_join 函数用于等待一个线程完成。在这个示例中,主线程等待两个子线程都完成后才继续执行。
请注意,编译此代码时需要使用 -lpthread 选项来链接Pthreads库。例如,在Linux上,你可以使用以下命令来编译:
gcc -o threads threads.c -lpthread
然后运行生成的可执行文件:
./threads
- 信号量解决同步问题 在 Linux 中,信号量(semaphores)是一种非常有用的同步机制,用于控制对共享资源的访问。它们可以确保多个线程或进程在访问共享资源时不会发生冲突。在 POSIX 线程(pthreads)编程中,通常使用 POSIX 信号量来实现这种同步。
以下是一些关于 Linux 信号量的常见问题和概念:
-
信号量的类型和初始化:
- POSIX 信号量有两种类型:无名信号量(基于内存)和有名信号量(基于文件系统)。
- 无名信号量通常使用
sem_t类型的变量和相关的函数(如sem_init,sem_wait,sem_post,sem_destroy)来操作。 - 有名信号量使用
sem_open,sem_close,sem_unlink等函数来操作,它们可以在进程间共享。
-
信号量的初始化和销毁:
- 使用
sem_init初始化无名信号量,指定信号量的值和进程/线程间共享的属性。 - 使用
sem_destroy销毁不再需要的无名信号量。 - 使用
sem_open创建有名信号量,并指定文件路径、权限和初始值。 - 使用
sem_unlink删除有名信号量对应的文件系统对象(但并不会立即销毁所有对它的引用)。
- 使用
-
信号量的等待和发布:
sem_wait(或sem_trywait)函数用于等待信号量的值大于零,并将信号量的值减一。如果信号量的值为零,则调用线程将被阻塞,直到信号量的值变为非零。sem_post函数用于增加信号量的值。通常,当一个线程完成对共享资源的访问后,会调用sem_post来释放信号量,允许其他线程访问。
-
死锁和避免:
- 信号量使用不当可能导致死锁,即两个或更多的线程无限期地等待一个资源,而这个资源被另一个线程所持有,而那个线程又在等待其他资源。
- 避免死锁的一种方法是确保线程在获取多个信号量时总是以相同的顺序进行。这被称为“顺序加锁”。
-
信号量的其他用途:
- 除了用于同步对共享资源的访问外,信号量还可以用于实现计数限制(如限制同时访问某个资源的线程数)或信号通知(如一个线程通知另一个线程某个事件已经发生)。
-
注意事项:
- 当使用信号量时,要特别注意初始值、增加和减少操作的顺序以及可能的错误情况(如
sem_wait返回 EINTR)。 - 信号量通常用于同步不同线程或进程之间的操作,而不是用于控制文件的读写权限。文件的读写权限通常由文件系统的权限设置和进程的用户/组 ID 来管理
- 当使用信号量时,要特别注意初始值、增加和减少操作的顺序以及可能的错误情况(如
在 Linux 下,可以使用 POSIX 信号量来实现线程之间的同步。下面是一个使用 POSIX 信号量实现线程间同步的简单示例:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
// 全局信号量
sem_t sem;
// 线程函数
void *thread_func(void *arg) {
// 等待信号量变为非零值
sem_wait(&sem);
// 访问共享资源或执行需要同步的操作
printf("Thread %ld is running\n", (long)arg);
// 释放信号量
sem_post(&sem);
return NULL;
}
int main() {
pthread_t threads[5];
int rc;
long t;
// 初始化信号量,值为1
sem_init(&sem, 0, 1);
// 创建5个线程
for(t = 0; t < 5; t++){
printf("In main: creating thread %ld\n", t);
rc = pthread_create(&threads[t], NULL, thread_func, (void *)t);
if (rc){
printf("ERROR; return code from pthread_create() is %d\n", rc);
exit(-1);
}
}
// 等待所有线程完成
for(t = 0; t < 5; t++){
pthread_join(threads[t], NULL);
}
// 销毁信号量
sem_destroy(&sem);
printf("Main thread exiting\n");
pthread_exit(NULL);
}
在这个示例中,我们创建了一个全局的 POSIX 信号量 sem,并将其初始化为 1。然后,我们创建了 5 个线程,每个线程都试图获取这个信号量(通过 sem_wait 调用)。由于信号量的初始值为 1,因此第一个线程会成功获取信号量并继续执行,而其他线程则会阻塞在 sem_wait 调用上,直到信号量被释放(通过 sem_post 调用)。当第一个线程完成其任务并释放信号量时,下一个线程会获取信号量并继续执行,依此类推。这样,我们就实现了线程之间的同步。
请注意,在实际使用中,你可能需要根据具体的需求来调整信号量的初始值和操作方式。此外,还需要注意处理可能出现的错误情况,如 sem_wait 或 sem_post 调用失败等。
- 生产消费者模型
在C语言中实现生产者-消费者模型通常涉及多线程编程和同步机制,如互斥锁(mutexes)和条件变量(condition variables)。以下是一个使用POSIX线程(Pthreads)库和互斥锁、条件变量实现的基本生产者-消费者模型的示例:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <stdbool.h>
#define BUFFER_SIZE 10
int buffer[BUFFER_SIZE];
int in = 0;
int out = 0;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t not_empty = PTHREAD_COND_INITIALIZER;
pthread_cond_t not_full = PTHREAD_COND_INITIALIZER;
void *producer(void *arg) {
for (int i = 0; i < 20; ++i) {
pthread_mutex_lock(&lock);
while ((in + 1) % BUFFER_SIZE == out) {
// 等待缓冲区不满
pthread_cond_wait(¬_full, &lock);
}
buffer[in] = i;
printf("Producer produced %d\n", i);
in = (in + 1) % BUFFER_SIZE;
pthread_cond_signal(¬_empty); // 通知消费者可能有数据可取
pthread_mutex_unlock(&lock);
}
return NULL;
}
void *consumer(void *arg) {
for (int i = 0; i < 20; ++i) {
pthread_mutex_lock(&lock);
while (in == out) {
// 等待缓冲区不为空
pthread_cond_wait(¬_empty, &lock);
}
int data = buffer[out];
printf("Consumer consumed %d\n", data);
out = (out + 1) % BUFFER_SIZE;
pthread_cond_signal(¬_full); // 通知生产者可能有空位可写
pthread_mutex_unlock(&lock);
}
return NULL;
}
int main() {
pthread_t prod, cons;
// 创建生产者和消费者线程
int rc1 = pthread_create(&prod, NULL, producer, NULL);
int rc2 = pthread_create(&cons, NULL, consumer, NULL);
if (rc1 || rc2) {
fprintf(stderr, "Error: return code from pthread_create() is %d\n", rc1);
exit(-1);
}
// 等待线程完成
pthread_join(prod, NULL);
pthread_join(cons, NULL);
return 0;
}
在这个示例中,我们定义了一个缓冲区 buffer,用于存储生产者产生的数据,以及两个索引 in 和 out,分别表示下一个要插入数据的位置和下一个要取出数据的位置。我们使用 pthread_mutex_t 类型的变量 lock 作为互斥锁,以保护对 buffer、in 和 out 的访问。此外,我们还使用 pthread_cond_t 类型的变量 not_empty 和 not_full 作为条件变量,分别用于在缓冲区为空时阻塞消费者线程,以及在缓冲区满时阻塞生产者线程。
生产者和消费者线程在运行时,会先获取互斥锁,然后检查缓冲区的状态。如果缓冲区已满(对于生产者)或为空(对于消费者),则调用 pthread_cond_wait 函数,释放锁并进入等待状态,直到另一个线程通过 pthread_cond_signal 或 pthread_cond_broadcast 函数将其唤醒。当缓冲区状态允许操作(不满对于生产者,不空对于消费者)时,线程会执行相应的操作,并更新缓冲区的状态。最后,线程会释放互斥锁,并可能唤醒其他正在等待的线程。