Skip to content

C语言杂项

基础数据类型

以32位系统为例:

数据类型 说明 字节
char 字符型 1
short 短整型 2
int 整型 4
long 长整型 4
float 单精度浮点型 4
double 双精度浮点型 8

sizeof和strlen

sizeof()计算的是变量的大小,strlen()计算的是字符串的长度。

char text[] = "abcdef";
sizeof(text); // 7
strlen(text); // 6

但是,如果C风格的字符串存储为char *,则sizeof()返回指针的大小。

指针

指针其实也是变量,只不过其中存储的是地址,由地址去间接地访问指向的变量。

指针变量最好在声明时就初始化,以避免访问野指针。

指针与数组

指针与数组非常相似,指针保存的是地址,数组名保存的是该数组的首地址。当我们把数组名赋值给指针时,实际上就是告诉了指针变量这个数组的首地址在哪,由于数组元素是连续存放的,挨个遍历就能访问整个数组元素。对指针的++操作,是编译器根据指针变量的长度为我们完成了地址的偏移。

要注意,数组名是地址,不可修改,而指针是个变量,加减操作是编译器替我们完成了地址的修改。

Question

指针数组和数组指针是什么?

int * p1[5];    //这是指针数组
int (*p2)[5];   //这是数组指针

指针数组,重点在数组,表明数组中的每个元素都是一个指针变量。

数组指针,重点在指针,表明该指针变量指向一个有5个元素的数组。

指针数组与数组指针

char *p1[2] = {"hello", "world"};
int temp[2] = {1, 2};
int (*p2)[2] = temp;

void指针

void指针其实是C语言中的多态,它可以指向任何一个类型的指针变量,而任何类型的指针变量都可以赋值给void指针。也就是说当你不知道对方要传入什么参数时,你就可以定义一个void指针来接收。

二级指针

二级指针实际上就是指向指针的指针,巧妙使用可以简化代码。

函数指针

函数指针即指向函数的指针,在C语言中,通常与回调函数一起使用。比如POSIX线程创建函数:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(start_routine)(void*), void *arg)start_routine

函数指针的定义方式为:函数返回值类型(*指针变量名)(函数参数列表)。

typedef void (*FunctionPtr)(cosnt char *);

void greet(const char *name)
{
    printf("Hello, %s!\n", name);
}

void goodbye(const char *name)
{
    printf("Goodbye, %s!\n", name);
}

void callFunction(FunctionPtr fp, const char *name)
{
    fp(name);
}

int main()
{
    const char *name = "world";
    callFunction(greet, name);
    callFunction(goodbye, name);
    return 0;
}

指针常量和常量指针

int value = 10;
const int *ptr = &value;    //这是一个指针常量
int *const ptr = &value;    //这是一个常量指针

指针常量,即指向的地址不可修改,但是可以修改指针指向的值。

常量指针,即指向的值不可修改,但是可以修改指针指向的地址。

结构体

对齐问题

32位系统采用4字节对齐,访问效率最高,不满4字节的会补齐。

struct person{
    char addr;
    char name;
    int id;
};

sizeof(struct person) = 8;

struct person {
    char addr;
    int id;
    char name;
};

sizeof(struct person) = 12;

位域

有些信息存储时并不需要占用整个字节,比如一个开关量只需要用1位代表开或者关。在结构体中使用位域可以有效节省内存空间。

struct person{
    unsigned char gender : 1;
    unsigned int age : 7;
};

gender表示性别,用1位表示男女,age表示年龄,用7位表示,最大为127,已经可以满足人年龄的需求了。这样结构体只需要一个字节就可以表示了。

printf

格式符 说明
%d 带符号十进制整数
%u 无符号十进制整数
%c 单个字符
%s 字符串
%f 浮点数
%x 无符号十六进制整数
%p 指针地址

extern

extern关键字用来告诉编译器,这里声明的变量或者函数名在别处定义,在此处声明。

Tip

定义:创建变量并分配内存空间 声明:说明变量的性质,并不分配内存空间

extern int i;   //此为声明
int i;         //此为定义

如果在.h头文件中声明一个全局变量,即为外部变量。外部变量保存在静态存储区,其生命周期存在于整个程序运行期,因此,外部变量不能出现重名,否则就会报重定义的错误。如果是内部变量,比如在函数内定义的变量,就不会出现这种问题。

多文件编程

在一个大型项目工程中,有的时候我们需要提前使用已经定义好的全局变量。

假设在"test.h"中:int a = 10;。这里a就是一个全局变量的定义,如果"test.h"头文件被多次引用,a就会被重复定义,这是不被允许的。

在《高质量C/C++编程指南》一书中写到:头文件中只存放“声明”,而不存放“定义”。

Question

如果我们需要在"main.c"中使用到该全局变量,应该怎么修改呢?

答案就是使用extern关键字。

在"test.h"中:extern int a;——这是一个声明。同时在"test.c"中:int a = 10;——这是一个定义。

这样在"main.c"中就可以直接引用变量a了。

头文件重复包含

#include命令在预处理阶段会直接被展开,预处理器将展开后的代码直接复制到.c文件中。如果被包含的头文件中还包含了其他的头文件,预处理器就会递归地包含这些头文件直到不再包含任何头文件。

递归包含会引入一个问题,就是重复地引入一个相同的头文件,这会导致变量重定义的错误。比如"test1.h"中定义了变量a,"test2.h"和"test3.h"都包含了"test1.h",那么编译的时候就会报错。

为了解决这个问题,C语言提供了#ifndef#define#endif三个预处理命令。这种宏保护方案使得程序员可以随意地引入当前模块需要的所有头文件,而不用操心这些头文件中是否包含了其他的头文件。

#pragma once可以用来替代以上指令。

extern "C"

这部分内容应该放在C++中,因为这是C++编译器特有的,不过既然写到extern了,就放在这里吧。

简单地说就是C++为了支持函数重载,会将函数名重新编码成一个全局唯一的符号,这样才能让链接器准确识别每个符号所对应的对象。而C语言不支持函数重载,因此对函数名不做复杂的处理。两种编译器不同的行为导致C++调用C编译器编译的函数时,会报"符号未定义"的错误。

为了解决这个问题,C++提供了extern "C"这个关键字,使得以extern "c"声明的函数名以C语言的形式被编译。同时由于该关键字是C++编译器特有的,所以在前面需要使用宏__cplusplus来判断当前编译器是否为C++编译器。

#ifdef __cplusplus
extern "C" {
#endif

...
#ifdef __cplusplus
}
#endif

注意:链接规范仅仅用于修饰函数和变量,以及函数类型。所以,严格地讲,你只应该把这三种对象放置于extern "C"的内部。

static

  1. 修饰局部变量:编译器为其初始化,存放于静态区,使得变量在文件内全局可见
  2. 修饰全局变量:普通的全局变量对整个工程可见,不可出现重名,静态全局变量只在当前文件可见
  3. 修饰全局函数:类似全局变量,静态函数只在当前文件可见

volatile

volatile的作用是消除优化,告诉编译器直接从内存中去取这个变量值。

事实上,Linux内核官方文档不推荐使用volatile。理由如下:内核提供了很多同步机制来保证并发访问时的数据安全,这些同步机制同样可以防止意外的优化。只要正确使用这些机制,就没有必要再使用volatile。如果仍然必须使用volatile,那么几乎可以肯定在代码的某处有一个bug。在正确设计的内核代码中,volatile只会降低系统的性能。

一段经典代码如下:

spin_lock(&the_lock);
do_something_on(&shared_data);
do_something_else_with(&shared_data);
spin_unlock(&the_lock);

当持有锁时,不可能意外改变shared_data的值,任何其他访问该值的代码都会等待锁的释放。就算shared_data被声明为volatile,在访问该变量时还是需要加锁。因此volatile被认为是没有必要的。

库函数

字符串函数:

函数格式 说明
strlen(s) 获取字符串长度,不包括"\0"
strcpy(dest, src) 拷贝src至dest
strcat(dest, src) 将src拼接到dest
strcmp(s1, s2) 比较字符串,相同则返回零
atoi(s1) 字符串转整数
memset(addr, val, n) 向目标地址填充n个val
memcpy(dest, src, n) 复制src的前n个字符到dest