C环境初始化
从start.S到C语言环境过程:
C语言的类型
在学习一门编程语言时要特别注意以下三个方面:
1. 这门语言提供了哪些Primitive,比如基本类型,比如基本运算符、表达式和语句。
2. 这门语言提供了哪些组合规则,比如基本类型如何组成复合类型,比如简单的表达式和语句如何组成复杂的表达式和语句。
3. 这门语言提供了哪些抽象机制,包括数据抽象和过程抽象(Procedure Abstraction)。
指针类型属于标量类型,因此也可以做逻辑与、或、非运算的操作数和if、for、while的控制表达式,NULL指针表示假,非NULL指针表示真。不完全类型是暂时没有完全定义好的类型,编译器不知道这种类型该占几个字节的存储空间,例如:
struct s;
union u;
char str[]; 具有不完全类型的变量可以通过多次声明组合成一个完全类型,比如数组str声明两次:
char str[];
char str[10]; 当编译器碰到第一个声明时,认为str是一个不完全类型,碰到第二个声明时str就组合成完全类型了,如果编译器处理到程序文件的末尾仍然无法把str组合成一个完全类型,就会报错。读者可能会想,这个语法有什么用呢?为何不在第一次声明时就把str声明成完全类型?有些情况下这么做有一定的理由,比如第一个声明是写在头文件里的,第二个声明写在.c文件里,这样如果要改数组长度,只改.c文件就行了,头文件可以不用改。
不完全的结构体类型有重要作用:
struct s {
struct t *pt;
};
struct t {
struct s *ps;
}; struct s和struct t各有一个指针成员指向另一种类型。编译器从前到后依次处理,当看到struct s { struct t* pt; };时,认为struct t是一个不完全类型,pt是一个指向不完全类型的指针,尽管如此,这个指针却是完全类型,因为不管什么指针都占4个字节存储空间,这一点很明确。然后编译器又看到struct t { struct s *ps; };,这时struct t有了完整的定义,就组合成一个完全类型了,pt的类型就组合成一个指向完全类型的指针。由于struct s在前面有完整的定义,所以struct s *ps;也定义了一个指向完全类型的指针。
这样的类型定义是错误的:
struct s {
struct t ot;
};
struct t {
struct s os;
}; 编译器看到struct s { struct t ot; };时,认为struct t是一个不完全类型,无法定义成员ot,因为不知道它该占几个字节。所以结构体中可以递归地定义指针成员,但不能递归地定义变量成员,你可以设想一下,假如允许递归地定义变量成员,struct s中有一个struct t,struct t中又有一个struct s,struct s又中有一个struct t,这就成了一个无穷递归的定义。
以上是两个结构体构成的递归定义,一个结构体也可以递归定义:
struct s {
char data[6];
struct s* next;
}; 当编译器处理到第一行struct s {时,认为struct s是一个不完全类型,当处理到第三行struct s *next;时,认为next是一个指向不完全类型的指针,当处理到第四行};时,struct s成了一个完全类型,next也成了一个指向完全类型的指针。类似这样的结构体是很多种数据结构的基本组成单元,如链表、二叉树等,我们将在后面详细介绍。下图示意了由几个struct s结构体组成的链表,这些结构体称为链表的节点(Node)。
指针运算
当表达式中同时存在有符号类型和无符号类型时,所有的操作数都自动转换为无符号类型
max (a, b)
char *a, *b;
{
if (a > b)
return(a);
return(b);
} 技巧:"a "和 "b "被声明为字符的指针,在比较中被视为无符号整数
double > float > unsigned long int > long int > unsigned int > int
在C语言中,各种类型的指针在进行不同类型的运算时,会被视为不同的类型进行运算。下面是各种类型指针在不同运算中被视为的类型:
- 算术运算:指针进行算术运算时,会根据指针指向的数据类型进行适当的偏移。例如,对指向
int类型的指针进行加法运算时,会按照sizeof(int)的大小进行指针的偏移。结果仍然是指针类型。 - 关系运算:指针进行关系运算(如
<、>、<=、>=)时,会被视为无符号整数类型进行比较。指针的数值将根据指针所指向的内存地址进行比较,而不是比较指针所指向的数据的值。 - 逻辑运算:指针在逻辑运算(如
&&、||)中,会被视为整数类型进行运算。指针的非零值被视为真(true),而空指针被视为假(false)。 - 位运算:指针进行位运算(如
&、|、^、<<、>>)时,会被视为无符号整数类型进行运算。指针的数值将被解释为无符号整数,并进行相应的位运算。 - 赋值运算:指针进行赋值运算时,会被视为相同类型的指针进行赋值。指针赋值运算将复制指针的值,使得两个指针指向相同的内存地址。
需要注意的是,以上的视为类型只是指针在进行相应运算时的处理方式。指针的实际类型仍然是指针类型,这些运算仅针对指针的值进行处理。此外,指针运算的结果仍然是指针类型,它们表示指向内存中特定位置的地址。
十进制、八进制和十六进制常量标志
85 /* 十进制 */
0213 /* 八进制 */
0x4b /* 十六进制 */
30 /* 整数 */
30u /* 无符号整数 */
30l /* 长整数 */
30ul /* 无符号长整数 */
30ull /* 无符号超长整数 */
const
const使用在两方面,一是在声明变量是使用,声明的同时赋予变量一个值,二是形容函数的形参变量,在函数被调用是得到实参的值
const int a和int const a相同
int const *pci; 指向整数常量的指针,可以修改指针的值,但不能修改它指向的值
int * const pci; 指向整形的常量指针,不能修改指针的值,但可以修改它指向的值
int const * const pci; 指针与变量皆为常量,都不能修改
#define
#define typecheck(type,x) \
({ type __dummy; \
typeof(x) __dummy2; \
(void)(&__dummy == &__dummy2); \
1; \
}) GCC的一个扩展特性,形如({ ... })这样的代码块会被视为一条语句,其计算结果是{ ... }中最后一条语句的计算结果。所以上述会返回1
作用域
- 代码块作用域
位于花括号之间的所有语句成为一个代码块,任何在代码块开始位置声明的标识符都具有代码块作用域
- 文件作用域
任何在代码块之外生命的表示符都具有文件作用域,表示这些标识符从它们的声明位置直到源文件结尾处都可以访问
- 原型作用域
原型作用域只适用于在函数原型中声明的参数名,声明函数原型可以不加参数名,如果给形参加名字可以任意取,不必与函数定义中的形参名匹配,也不必与函数实际调用时所传递的实参匹配
- 函数作用域
只适用与语句标签,用于goto语句
链接属性
none 总是被当作单独的个体,该标识符的多个声明被当作独立不同的实体
internal 位于同一个源文件中的所有声明都属于一个实体,不同源文件中的多个声明分属不同的实体
external 无论声明多少次、位于几个源文件都表示同一个实体
未初始化的全局变量属于bss段,bss段在文件中没有具体实体,只有声明的大小,在执行时分配固定大小内存,内存中的bss段占用为bss段分配的固定长度的内存空间
存储类型
变量的存储类型取决于变量何时创建、何时销毁以及它的值保持多久,有三个地方可以存储变量:普通内存、运行时堆栈、硬件寄存器
变量的缺省存储类型取决于它的声明位置,凡是在任何代码块之外声明的变量总是存储在静态内存中,也就是不属于堆栈的内存,这类变量称为静态(static)变量,无法为他指定其他存储类型,而且变量在程序运行之前创建
代码块内部声明的变量的缺省存储类型是自动(automatic)的,也就是说他存储与堆栈中,成为自动(atuo)变量
在代码块内部声明的变量如果加上static则存储类型从自动变成静态,具有静态存储类型的变量在整个程序执行过程中一直存在,但是并不改变变量的作用域(仍然只能在该代码块内部按名字访问),注意:函数的形参不能声明为静态,因为实参总在堆栈中传递给函数,用于支持递归
register用于自动变量的声明,提示它们应存储于硬件寄存器而不是内存
静态变量初始化中,可以把可执行程序文件想要初始化的值放到程序执行时变量将会使用的位置,程序执行时将把保存了初始值的位置赋给那个变量。静态变量如果不显式指定其初始值,将初始化为0
static
用于函数定义或代码块之外的变量声明时,static关键字用于修改标识符的链接属性,从external改为internal,但标识符的存储类型和作用域不受影响,声明函数或变量只能在该源文件中访问
当它用于代码块内部的变量声明时,static关键字用于修改变量的存储类型,从自动变量变为静态变量,但变量的链接属性和作用域不受影响,声明变量在程序执行前创建,并在程序的整个执行期间一直存在
数组
在C语言中,数组的初始化有以下几种方式:
静态初始化:在声明数组时,使用大括号{}将元素的初始值按顺序包含在内。例如:
int arr[] = {1, 2, 3, 4, 5}; 动态初始化:在声明数组后,逐个为数组的元素赋值。例如:
int arr[5];
arr[0] = 1;
arr[1] = 2;
arr[2] = 3;
arr[3] = 4;
arr[4] = 5; 部分初始化:可以仅对数组的一部分元素进行初始化,未初始化的元素将被默认设置为0。例如:
int arr[5] = {1, 2}; // 前两个元素初始化为1和2,后三个元素默认为0 字符串初始化:对于字符数组,可以使用字符串字面值进行初始化。例如:
char str[] = "Hello"; 使用指定的索引进行初始化:可以使用特定索引指定数组中的元素初始化值。其他未指定的元素将被默认初始化为0。例如:
int arr[10] = {[2] = 5, [4] = 10}; // 索引为2的元素初始化为5,索引为4的元素初始化为10,其他元素默认为0
可变参数实现
#include <stdio.h>
typedef char* va_list;
#define va_start(ap, last_arg) (ap = (va_list)&last_arg + sizeof(last_arg))
#define va_arg(ap, type) (*(type*)((ap += sizeof(type)) - sizeof(type)))
#define va_end(ap) (ap = NULL)
int sum(int num_args, ...)
{
int total = 0;
va_list args;
va_start(args, num_args);
for (int i = 0; i < num_args; i++)
{
int arg = va_arg(args, int);
total += arg;
}
va_end(args);
return total;
}
int main()
{
int result = sum(3, 10, 20, 30);
printf("Sum: %d\n", result);
return 0;
}
函数并发安全性标记
- MT-Safe(Multi-Thread Safe,多线程安全):一个被标记为MT-Safe的函数在多线程环境中可以被多个线程同时调用而不会产生竞争条件或导致数据损坏。这意味着该函数在并发环境下是线程安全的,并且可以被多个线程并发地访问和使用。
- AS-Safe(Async-Signal Safe,异步信号安全):一个被标记为AS-Safe的函数可以被在信号处理程序中调用,而不会导致不确定的行为。当一个进程收到信号时,信号处理程序可能在任何时间点中断正在执行的函数,并在信号处理程序中调用其他函数。AS-Safe函数能够安全地在信号处理程序中使用,而不会导致竞争条件或其他问题。
- AC-Safe(Async-Cancel Safe,异步取消安全):一个被标记为AC-Safe的函数可以在多线程环境中安全地与取消点(cancellation point)一起使用。取消点是指程序中的某个位置,在该位置处可以被取消请求终止线程的执行。AC-Safe函数能够在取消点被取消的情况下正确地处理资源释放和状态维护,以避免资源泄漏或数据损坏。
例子:
MT-Safe(多线程安全):
-
printf函数:它是一个MT-Safe函数,可以在多个线程中并发调用,因为它内部使用了互斥锁来确保输出的正确性。 -
strlen函数:它是一个MT-Safe函数,可以在多个线程中并发调用,因为它只是对字符串进行计数,没有共享状态。
AS-Safe(异步信号安全):
-
write函数:它是一个AS-Safe函数,可以在信号处理程序中调用。因为它是一个系统调用函数,不会引发竞争条件或数据访问问题。 -
abort函数:它是一个AS-Safe函数,可以在信号处理程序中调用,用于终止程序的执行。
AC-Safe(异步取消安全):
-
pthread_mutex_lock函数:它是一个AC-Safe函数,在加锁的过程中可以被取消,而且能够在取消点处正确地处理资源释放和状态维护。 -
pthread_cond_wait函数:它是一个AC-Safe函数,当线程处于条件变量等待状态时,可以被取消,并且能够在取消点处正确地处理资源释放和状态维护。
C 程序的内存布局
C 程序的典型内存表示形式由以下部分组成。
- Text segment :包含可执行指令
- Initialized data segment:包含程序员初始化的全局变量和静态变量,该段可进一步分为初始化的只读区和初始化的读写区。
- Uninitialized data segment (bss):包含所有初始化为零或在源代码中没有显式初始化的全局变量和静态变量
- Heap:堆是通常进行动态内存分配的段,从 BSS 段的末尾开始,堆区域由进程中所有共享库和动态加载的模块共享。
- Stack:用于存储自动变量以及每次调用函数时保存的信息。每次调用函数时,返回位置的地址和有关调用方环境的某些信息(例如某些计算机寄存器)都会保存在堆栈中。