Contents

C语言中的关键字

关键字大体上分为三大类:数据类型关键字、修饰关键字、逻辑关键字。

sizeof 关键字

sizeof(x):编译器给我们查看内存空间容量的工具,在编译时计算并确定x在内存中所占用的字节数。

sizeof(常量/变量/数据类型)

  • sizeof 传入参数可以是常量、变量和数据类型
#include <stdio.h>

int main() {
    int a = 10;

    printf("Size of 10 is: %ld\n", sizeof(10));
    printf("Size of a is: %ld\n", sizeof(a));
    printf("Size of int is: %ld\n", sizeof(int));

    return 0;
}

输出结果:

Size of 10 is: 4
Size of a is: 4
Size of int is: 4

sizeof(函数调用)

  • sizeof 传入函数调用时,计算的是函数返回值的类型所占用的字节数,函数本身不会被调用(印证了sizeof在编译时计算出空间大小,而不是运行时)。
#include <stdio.h>

int getValue() {
    printf("getValue() called\n");
    return 42;
}

int main() {
    printf("Size of getValue() is: %ld\n", sizeof(getValue()));
    return 0;
}

输出结果:

Size of getValue() is: 4

getValue() called 并没有被打印出来,说明函数并没有被调用。

sizeof(数组)

  • sizeof 传入数组时,计算的是整个数组所占用的字节数
#include <stdio.h>
int main() {
    int arr[10];

    printf("Size of arr is: %ld\n", sizeof(arr));
    return 0;
}

输出结果:

Size of arr is: 40

数据类型关键字

标准数据类型关键字:

char

  • 最小内存空间的数据类型,通常占用1个字节(8位),用于存储字符数据。当编译器看到char关键字时,会分配1个字节的内存空间。
#include <stdio.h>

int main() {
    char a = 9;
    printf("a = %d, size of a is: %ld byte\n", a, sizeof(a));
    return 0;
}
  • 输出结果:
a = 9, size of a is: 1 byte
  • char 是一种整数类型,可以存储整数值或字符的 ASCII 值
    • 当赋值为整数时,char 会直接存储该整数。
    • 当赋值为字符时,char 会存储字符的 ASCII 值。
#include <stdio.h>

int main() {
    char a = 65; // ASCII value for 'A'
    char b = 'A'; // Character 'A'

    printf("a = %d, b = %d\n, ASCII of a/b is %c/%c\n", a, b, a, b); // Both will print 65
    return 0;
}

输出结果:

a = 65, b = 65, ASCII of a/b is A/A

int

最适合CPU的数据类型,大小跟编译器有关。 /images/C语言/image.png

为了充分发挥CPU的数据处理能力,数据总线尽量要充分使用,在系统一个周期内所能接受的最大处理单位是int。在16位系统中,int通常占用2个字节;在32位和64位系统中,int通常占用4个字节。

#include <stdio.h>

int main() {
    int a = 10;

    printf("Size of 10 is: %ld\n", sizeof(10));
    printf("Size of a is: %ld\n", sizeof(a));
    printf("Size of int is: %ld\n", sizeof(int));

    return 0;
}
  • 输出结果:
Size of 10 is: 4
Size of a is: 4
Size of int is: 4

short/long

  • short:短整型,通常占用2个字节(16位),用于表示较小范围的整数。
  • long:长整型,通常占用8个字节(64位),用于表示较大范围的整数。
#include <stdio.h>

int main() {
    short int a = 10;
    short b = 20;

    long int c = 30;
    long d = 40; 

    printf("Size of short int a b c d is: %ld %ld %ld %ld\n", \
            sizeof(a), sizeof(b), sizeof(c), sizeof(d));

    return 0;
}

输出结果:

Size of short int a b c d is: 2 2 8 8

float/double

  • float:单精度浮点数,通常占用4个字节(32位),用于表示小数。它的有效位数约为7位十进制数字。
#include <stdio.h>
int main() {
    float a = 3.14f;

    printf("a = %f, size of a is: %ld byte\n", a, sizeof(a));
    return 0;
}
  • 输出结果:
a = 3.140000, size of a is: 4 byte
  • double:双精度浮点数,通常占用8个字节(64位),用于表示更大范围和更高精度的小数。它的有效位数约为15-16位十进制数字。
#include <stdio.h>
int main() {
    double a = 3.141592653589793;

    printf("a = %lf, size of a is: %ld byte\n", a, sizeof(a));
    return 0;
}

signed/unsigned

  • signed:表示有符号类型,可以存储正数、负数和零。默认情况下,整数类型(如intchar)都是有符号的。最高位代表正负(1代表负, 0代表正),区间范围为 -2^(n-1) 到 2^(n-1)-1(n为位数)。一般情况下,我们不需要显式地使用signed关键字,因为它是默认的。
  • unsigned:表示无符号类型,只能存储非负数(正数和零)。使用无符号类型可以扩大可表示的正整数范围,但不能表示负数。

void

  • void:表示无类型,通常用于函数返回类型,表示函数不返回任何值;也可以用于指针类型,表示指向未知类型的数据
void func() {
    // This function does not return any value
}
  • void*:表示通用指针类型,可以指向任何数据类型的地址,但不能直接进行解引用操作,必须先转换为具体类型的指针

memcpy函数的第一个参数就是void*类型,可以传入任何类型的指针。通过man memcpy可以查看其定义:

#include <string.h>

void *memcpy(void dest[restrict .n], const void src[restrict .n],
                    size_t n);

memcpy为例,void*指针以字节为单位进行内存操作:

#include <stdio.h>
#include <string.h>

int main() {
    int a = 0;
    int b = 0x87654321; // Hexadecimal representation
    memcpy(&a, &b, 1); // Copy bytes from b to a
    printf("a = %x\n", a); // Print a in hexadecimal format
    return 0;
}

/images/C语言/image2.png

自定义类型

C语言默认定义的数据类型不满足实际内存分配需求时,通过组合来形成新的类型。本质上是支持程序员根据实际需求分配内存大小

struct 结构体

C语言通过struct关键字定义结构体类型,结构体是由多个不同类型的数据成员组成的复合数据类型,内存表现为累加且对齐

  • 声明规则如下:
struct 结构体名 {
    数据类型 成员名1;
    数据类型 成员名2;
    ...
};

struct abc {
    int a;
    char b;
}
这里只是声明了结构体类型,并没有分配内存空间。
  • 结构体的初始化和访问
#include <stdio.h>

struct abc {
    int a;
    char b;
};

int main() {
    struct abc c = {
        .a = 10,
        .b = 'c'
    };

    printf("Size of struct abc is: %ld\n", sizeof(c));
    printf("a = %d, b = %c\n", c.a, c.b);

    return 0;
}

输出结果:

Size of struct abc is: 8
a = 10, b = c

/images/C语言/image3.png

  • 在定义结构体变量时,编译器默认会把首地址对齐到4字节(跟具体体系结构有关,也可能是2个字节对齐)的倍数:首地址对齐

为什么需要内存对齐:1. 提高CPU访问效率,CPU 以固定字长(32位系统4字节;64位系统8字节)为单位访问内存,对齐的数据可一次读取,未对齐则需多次拼接,效率大幅降低;2. 硬件限制:部分CPU硬件不支持非对齐访问,直接引发异常。

  • 边界对齐
    • 部分CPU硬件支持非对齐访问,典型的是x86,x86硬件会自动处理对齐访问情况,对软件透明,代价是牺牲效率。
    • 部分CPU“部分支持”非对齐访问,典型的就是ARM,其“单指令”操作(处理器一次只执行一条指令,通常用于标量操作,对单个数据进行操作)非对齐,但“群指令”操作(SIMD,处理器一次操作多个数据,通常用于向量化操作)则不支持(必须对齐访问)
    • 部分CPU硬件不支持对齐访问,但通过软件支持。典型的是部分mips架构。

结构体中成员定义的顺序会影响内存对齐和空间利用率,合理安排成员顺序可以减少内存浪费

#include <stdio.h>

struct abc {
    char b;
    char c;
    int a;
};

struct dfg {
    char b;
    int a;
    char c;
};

int main() {

    printf("Size of struct abc is: %ld, cfg is: %ld\n", sizeof(struct abc), sizeof(struct dfg));

    return 0;
}

输出结果:

Size of struct abc is: 8, cfg is: 12

/images/C语言/image4.png

union 联合体

内存表现为共享一份内存(以最大数据类型作为分配空间)和内存的首地址(低位),声明的规则如下:

union 联合体名 {
    数据类型 成员名1;
    数据类型 成员名2;
    ...
};

联合体的初始化和访问

#include <stdio.h>

union abc {
    int a;
    char b;
};

int main() {
    union abc c;
    c.b = 1;

    printf("Size of union abc is: %ld\n", sizeof(c));
    printf("a = %x, b = %x\n", c.a, c.b);

    c.b = 100;
    printf("a = %x, b = %x\n", c.a, c.b);

    return 0;
}

输出结果:

Size of union abc is: 4
a = 1, b = 1
a = 64, b = 64

/images/C语言/image5.png

enum 枚举类型

enum关键字表示的类型叫枚举类型,内存表现为:内存根据定义值的大小默认选择整数常量大小(int),如果超出int大小,编译器会选择更大的数据类型(long)。

声明规则如下:

enum 枚举类型名 {
    标识符1 = 常量值1,
    标识符2 = 常量值2,
    ...
};
  • enum的初始化和访问
#include <stdio.h>

enum abc {
    A = 1,
    B = 2,
    C = 3,
    D
};

enum efg {
    E = 0x123456789,
    F,
    G
};

enum klm {
    K = 'a',
    L,
    M
};

int main() {

    printf("a = %d, b = %d, c = %d, d = %d\n", A, B, C, D);
    printf("k = %c, l = %c, m = %c\n", K, L, M);
    printf("Size of A = %ld, abc = %ld\n", sizeof(A), sizeof(enum abc));
    printf("Size of E = %ld, efg = %ld\n", sizeof(E), sizeof(enum efg));
    printf("Size of K = %ld, klm = %ld\n", sizeof(K), sizeof(enum klm));

    return 0;
}

输出结果:

a = 1, b = 2, c = 3, d = 4
k = a, l = b, m = c
Size of A = 4, abc = 4
Size of E = 8, efg = 8
Size of K = 4, klm = 4

地址(指针)类型

C语言中,地址类型也称为指针类型,圈定的内存用来存放地址编号的值。指针类型的内存表现跟编译器有关或者跟CPU的地址总线有关。 在32位的系统中,指针类型占用4byte,在64位的系统中,指针类型占用8byte。

声明规则如下

数据类型 *指针变量名;
  • 指针类型的初始化和访问
#include <stdio.h>

int main() {
    int a = 10;
    int *p = &a;

    printf("symbol visit: a = %d, addr visit: a = %d\n", a, *p);
}

输出结果:

symbol visit: a = 10, addr visit: a = 10

typedef 关键字

typedef关键字用于为已有的数据类型定义新的类型名称,简化代码书写,提高代码可读性。

  • 声明规则如下:
typedef 现有数据类型 新类型名称;
typedef struct 结构体名 新类型名称;
typedef union 联合体名 新类型名称;
typedef enum 枚举类型名 新类型名称;
  • typedef的使用示例
#include <stdio.h>

typedef struct {
    int x;
    int y;
} Point_t;

typedef int Integer_t;
typedef unsigned char uint8_t;
typedef void (*func)(void);

func p;

int main() {
    Point_t p = {10, 20};
    uint8_t byte = 255;

    printf("Point p: x = %d, y = %d\n", p.x, p.y);
    return 0;
}
  • typedef#define 的区别
    • typedef类型定义,不会占用内存空间,只是为类型起了一个别名。
    • #define预处理指令,会在编译前进行文本替换,可能会导致代码膨胀,占用更多内存。
#include <stdio.h>

#define uchar_p unsigned char* 
typedef unsigned char* uint8_p;

int main() {
    uchar_p a, b;
    uint8_p c, d;

    a = "hello";
    b = "hello";
    c = "world";
    d = "world";

    return 0;
}

编译报错:

1.c: In function ‘main’:
1.c:11:7: warning: assignment to ‘unsigned char’ from ‘char *’ makes integer from pointer without a cast [-Wint-conversion]
   11 |     b = "hello";

说明#define uchar_p unsigned char*在预处理阶段被替换为unsigned char* a, b;,导致b被定义为unsigned char类型,而不是指针类型。


修饰关键字

auto关键字

auto 关键字用于声明自动变量,表示变量的存储类型为自动存储类型。自动变量在函数或代码块内定义,生命周期仅限于该函数或代码块执行期间,离开作用域后自动销毁。auto关键字是C语言中的默认存储类型,因此通常不需要显式使用。

register关键字

register 关键字用于建议编译器将变量存储在CPU的寄存器中,以提高访问速度。由于寄存器数量有限,编译器可能会忽略该建议。register 关键字只能用于局部变量,不能用于全局变量或静态变量。加上register关键字的变量不能获取其地址(不能使用取地址符&,即使大概率编译器也不会将其放入寄存器中。

#include <stdio.h>

int main() {
    register int count = 0;

    printf("Count: %d, addr of Count: %p\n", count, &count); 
    return 0;
}

编译报错:

error: address of register variable ‘count’ requested
    6 |     printf("Count: %d, addr of Count: %p\n", count, &count);

static关键字

static 关键字用于声明静态变量,表示变量的存储类型为静态存储类型。静态变量在程序运行期间只初始化一次,生命周期贯穿整个程序运行过程。静态变量可以是局部变量,也可以是全局变量。

  • 限定变量的内存从静态全局数据区分配
  • 限定变量或函数的作用域

修饰变量

  • 生命周期:不管是修饰局部变量还是全局变量,静态变量的生命周期都是整个程序运行期间
#include <stdio.h>

static int a = 20;
int func() {
    static int b = 10;
    return ++b;
}

int main() {
    printf("a = %d, b = %d, b = %d", a, func(), func());
}

输出结果:

a = 20, b = 12, b = 11

b的值在函数调用之间得以保留。注意到打印出来的b先是12,后是11,C 标准未定义(Unspecified)函数参数的求值顺序,这里用的编译器gcc是从右到左求值的。

  • 限定范围:限定全局变量只在当前定义的文件可见,其他文件即使加上extern关键字也无法访问该变量。
// 1.c  
#include <stdio.h>

static int a = 20; // 静态全局变量

int func() {
    return ++a;
} 
int main() {
    printf("a = %d\n", a);
    return 0;
}

// 2.c

extern int a; // 尝试访问1.c中的静态全局变量
static int func() {
    return ++a;
} 

编译报错:

in function `func':
2.c:(.text+0xa): undefined reference to `a'

extern关键字

  • extern 关键字用于声明外部变量或函数,表示该变量或函数在其他文件中定义。extern 关键字不会分配内存空间,只是告诉编译器该变量或函数的定义在其他地方。

  • extern "C", 用C++的编译器(g++)按照C的规则进行编译。C++和C语言在编译规则上有很多不同,所以在C++中想完全复用C写的代码,就可以用extern "C"来告诉编译器按照C的规则去编译。在C++使用C的lib库的时候比较常见。

const关键字

  • const 关键字用于声明常量,表示变量的值在初始化后不能被修改。实际上有一些方法可以做到修改const修饰变量的值,往往都是一些异常的操作导致(比如数组越界、指针越界访问、栈溢出等),编译器尽量不让我们修改。 const 可以修饰基本数据类型、指针类型、结构体等
const 数据类型 变量名 = 初始值; or 数据类型 const 变量名
#include <stdio.h>

int main() {
    const int a = 10;
    int const b = 20;

    // a = 15; // 编译报错:assignment of read-only variable 'a'
    printf("a = %d, b = %d\n", a, b);
    return 0;
}
  • 修改const变量的值
#include <stdio.h>

int main() {
    const int a = 10;
    int *p = (int*)&a; // 强制类型转换,去掉const属性
    *p = 20; // 修改值

    printf("a = %d\n", a); // 输出结果可能是10或20,取决于编译器优化
    return 0;
}

通过地址越界修改const变量的值(C++可以防止)

#include <stdio.h>  

int main() {
    int a = 123456;
    const int b = 111;
    int *p = &a;


    p[1] = 222; // 修改a后面的内存单元
    printf("b = %d\n", b);
    return 0;
}
  • const修饰指针

const修饰指针时,有两种情况:

  1. 指向常量的指针(指针所指向的值不可修改)

    const int * a;
    int const * b;
  2. 常量指针(指针本身的地址不可修改)

    int * const c;
  3. 常量指针指向常量(指针本身和指针所指向的值都不可修改)

    const int * const d;
#include <stdio.h>

const int * a;
int const * b;

int * const c;
const int * const d;

int main() {
    *a = 1;
    *b = 2;
    c = 3;
    *d = 4;
    d = 1;
    return 0;
}

编译报错:

3.c: In function main:
3.c:10:8: error: assignment of read-only location *a
   10 |     *a = 1;
      |        ^
3.c:11:8: error: assignment of read-only location *b
   11 |     *b = 2;
      |        ^
3.c:12:7: error: assignment of read-only variable c
   12 |     c = 3;
      |       ^
3.c:13:8: error: assignment of read-only location *(const int *)d
   13 |     *d = 4;
      |        ^
3.c:14:7: error: assignment of read-only variable d
   14 |     d = 1
      |       ^
3.c:14:10: error: expected ; before return
   14 |     d = 1
      |          ^
      |          ;
   15 |     return 0;
      |     ~~~~~~
  • 使用const可以提高运行效率:如果用const修饰,编译器会直接把立即数赋值给变量,而没有const修饰的则每次都需要读取内存中ABC值
#include <stdio.h>

const int ABC = 200;

int main() {

    int a = ABC;
    return 0;
}

转换为汇编代码:

    // const int ABC = 200;
    movl	$200, -4(%rbp)
    // int ABC = 200;
    movl	ABC(%rip), %eax
	movl	%eax, -4(%rbp)x
  • const#define 的区别
    • 编译器处理方式define是在预处理阶段进行文本替换;而const是在编译阶段进行类型检查和内存分配。
    • 安全检查define没有类型检查,可能导致类型错误;而const会进行类型检查,确保类型安全。
    • 内存位置define定义的常量在代码段,没有固定的内存位置;而const定义的常量可以在静态全局数据段、栈中。

volatile关键字

volatile 关键字用于告诉编译器该变量的值可能会在程序的控制之外发生变化,因此编译器不应该对其进行某些优化。主要用于处理硬件寄存器、中断服务程序和多线程等情况。

  • 并行设备的硬件寄存器。存储器映射的硬件寄存器通常加volatile,因为寄存器随时可以被外设硬件修改。当声明指向设备寄存器的指针时一定要用volatile,它会告诉编译器不要对存储在这个地址的数据进行假设。
  • 一个中断服务程序中修改的供其他程序检测的变量。volatile提醒编译器,它后面所定义的变量随时都有可能改变。因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。
  • 多线程应用中被几个任务共享的变量
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

int wait = 1;

void* function(void* arg) {
    while(1) {
        sleep(1);
        wait = 0;
        printf("flag wait = %d in thread \n", wait);
    }
    return NULL;
}

int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, function, NULL);

    while(wait);
    printf("Exited loop in main thread, wait = %d\n", wait);
    return 0;
}
  • 编译:
gcc -o bin 1.c -lpthread
  • 输出结果:
flag wait = 0 in thread
Exited loop in main thread, wait = 0
  • 开启编译器优化:
gcc -o bin 1.c -lpthread -O3
  • 输出结果:
flag wait = 0 in thread 
flag wait = 0 in thread 
flag wait = 0 in thread 
flag wait = 0 in thread 
flag wait = 0 in thread 
flag wait = 0 in thread 
... (程序卡死在这里)

程序卡死在主线程的while(wait);循环中,原因是编译器在优化时假设wait变量在主线程中不会被修改,因此将其值缓存到寄存器中,导致主线程无法检测到子线程对wait变量的修改。

  • 加上volatile关键字:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

volatile int wait = 1;

void* function(void* arg) {
    while(1) {
        sleep(1);
        wait = 0;
        printf("flag wait = %d in thread \n", wait);
    }
    return NULL;
}

int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, function, NULL);

    while(wait);
    printf("Exited loop in main thread, wait = %d\n", wait);
    return 0;
}

程序输出结果:

flag wait = 0 in thread
Exited loop in main thread, wait = 0

逻辑关键字

在C语言中,程序指针(PC)指到哪里就执行哪里,默认情况下顺序往下执行,而逻辑关键字作用就是改变PC指针的指向,这个最基本的思想就构成了程序里各种逻辑设计(条件、选择、跳转、循环)。

条件:if、else

条件逻辑关键字,判断条件表达式是否为真,为真后停止判断并进入处理对应逻辑。

if (condition) {
    // 条件为真时执行的代码
} else {
    // 条件为假时执行的代码
}

选择:switch、case、default

选择逻辑关键字,根据表达式的值选择执行对应的代码块。

switch (expression) {
    case value1:
        // 当expression等于value1时执行的代码
        break;
    case value2:
        // 当expression等于value2时执行的代码
        break;
    default:
        // 当expression不匹配任何case时执行的代码
}

这里的expression可以是整数类型或枚举类型,case后面跟的是具体的值。 For example:

#include <stdio.h>

enum Day {
    SUNDAY,
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY
};

int main() {
    enum Day today = WEDNESDAY;

    switch (today) {
        case SUNDAY:
            printf("Today is Sunday\n");
            break;
        case MONDAY:
            printf("Today is Monday\n");
            break;
        case TUESDAY:
            printf("Today is Tuesday\n");
            break;
        case WEDNESDAY:
            printf("Today is Wednesday\n");
            break;
        case THURSDAY:
            printf("Today is Thursday\n");
            break;
        case FRIDAY:
            printf("Today is Friday\n");
            break;
        case SATURDAY:
            printf("Today is Saturday\n");
            break;
        default:
            printf("It's some other day\n");
            break;
    }

    return 0;
}

循环:for、while、do…while

  • for循环:用于已知循环次数的情况。
for (int i = 0; i < n; i++) {
    // 循环体
}
  • while循环:用于循环次数不确定的情况,先判断条件再执行循环体。
while (condition) {
    // 循环体
}
// 实例
#include <stdio.h>
int main() {
    int count = 0;
    while (count < 5) {
        printf("Count is: %d\n", count);
        count++;
    }
    return 0;
}
  • do…while循环:先执行循环体,再判断条件,至少执行一次。
do {
    // 循环体
} while (condition);
// 实例
#include <stdio.h>
int main() {
    int count = 0;
    do {
        printf("Count is: %d\n", count);
        count++;
    } while (count < 5);
    return 0;
}

跳转:break、continue、return、goto

  • break:用于跳出当前循环或switch语句。
for (int i = 0; i < 10; i++) {
    if (i == 5) {
        break; // 当i等于5时跳出循环
    }
    printf("%d\n", i);
}
  • continue:用于跳过当前循环的剩余部分,进入下一次循环。
for (int i = 0; i < 10; i++) {
    if (i % 2 == 0) {
        continue; // 跳过偶数
    }
    printf("%d\n", i); // 只打印奇数
}
  • return:用于从函数中返回,并可选择性地返回一个值。
int add(int a, int b) {
    return a + b; // 返回a和b的和
}
int main() {
    int sum = add(3, 4);
    printf("Sum is: %d\n", sum);
    return 0;
}
  • goto:用于无条件跳转到程序中的另一个位置,通常不推荐使用,因为会导致代码难以阅读和维护。Linux源码中有大量使用,可以用来实现错误处理和资源释放。
goto label;

label:
// 跳转到这里执行的代码