【C语言入门】解锁核心关键字的终极奥秘与实战应用(二)

40 阅读31分钟

一、sizeof

1.1 作用

  • 数据类型:sizeof 可以直接作用于基本数据类型(如 int, char, float, double 等)以及用户定义的数据类型(如结构体、联合体等)。

  • 变量:sizeof 也可以作用于变量,此时它返回的是该变量类型所占的内存大小,而不是变量的值。

  • 编译时计算:sizeof 的计算是在编译时进行的,而不是在运行时。意味着它不会增加程序的运行时间开销。

  • 括号:在使用 sizeof 时,通常建议将其操作数放在括号中,以避免潜在的解析歧义。例如,sizeof(int) 而不是 sizeof int。

  • 指针:当 sizeof 作用于指针时,它返回的是指针类型本身所占的内存大小,而不是指针所指向的数据的大小。指针大小依赖系统位数,64位系统中通常为8字节,32位系统为4字节,16位系统为2字节。

结构体计算需遵循内存对齐规则,对齐数为编译器默认对齐数与成员大小的较小值(VS默认8,GCC默认4),结构体总大小为最大对齐数的整数倍。

1.2 代码示例

示例1:基本****数据类型

#include <stdio.h>  
  
int main() {  
    printf("Size of int: %zu bytes\n", sizeof(int));  
    printf("Size of char: %zu bytes\n", sizeof(char));  
    printf("Size of float: %zu bytes\n", sizeof(float));  
    printf("Size of double: %zu bytes\n", sizeof(double));  
    return 0;  
}

运行结果:

sizeof 被用来计算基本数据类型 int, char, float, 和 double 的大小,并将结果打印出来。

示例2:变量

#include <stdio.h>  
  
int main() {  
    int a = 10;  
    char b = 'c';  
    printf("Size of variable a (int): %zu bytes\n", sizeof(a));  
    printf("Size of variable b (char): %zu bytes\n", sizeof(b));  
    // 或者直接使用变量类型  
    printf("Size of type of variable a: %zu bytes\n", sizeof(int));  
    printf("Size of type of variable b: %zu bytes\n", sizeof(char));  
    return 0;  
}

运行结果:

sizeof 被用来计算变量 ab 的大小,分别是 int 类型和 char 类型。注意,sizeof(a)sizeof(int) 返回的是相同的结果。

示例3:指针

#include <stdio.h>  
  
int main() {  
    int *ptr = NULL;  
    printf("Size of pointer: %zu bytes\n", sizeof(ptr));  
    // 注意:sizeof(*ptr) 将返回 ptr 所指向的 int 类型的大小  
    printf("Size of type pointed to by ptr: %zu bytes\n", sizeof(*ptr));  
    return 0;  
}

运行结果(64位系统):

sizeof(ptr) 返回的是指针 ptr 本身所占的内存大小,而 sizeof(*ptr) 返回的是 ptr 所指向的 int 类型的大小。

示例4:结构体

#include <stdio.h>  
  
struct MyStruct {  
    int a;  
    char b;  
    double c;  
};  
  
int main() {  
    struct MyStruct s;  
    printf("Size of struct MyStruct: %zu bytes\n", sizeof(struct MyStruct));  
    printf("Size of variable s of type struct MyStruct: %zu bytes\n", sizeof(s));  
    return 0;  
}

运行结果:

sizeof 被用来计算结构体 MyStruct 的大小。注意,结构体的大小可能会因为内存对齐(padding)而大于其成员大小的总和。

二、const

const 关键字用于定义常量,即初始化后值不能修改的变量,能提高代码可读性和安全性,编译器会在编译时检查非法修改。

2.1 作用

  1. 定义常量:const 修饰的变量必须在声明时初始化,之后其值就不能被改变了。

  2. 类型安全:通过 const,编译器可以在编译时检查对常量的非法修改,从而提高代码的类型安全性。

  3. 作用域:const 常量的作用域取决于其声明位置。在函数内部声明的 const 常量具有局部作用域,而在文件范围或全局范围内声明的 const 常量则具有相应的全局作用域。

  4. 指针与 const

    • 指针指向常量:const 修饰指向的类型,不能通过指针修改所指内容,但可更改指针指向。

    • 指针本身为常量:const 修饰指针变量,不能更改指针指向,但可修改所指内容(所指对象非 const 时)。

    • 指针指向常量且本身为常量:既不能更改指针指向,也不能通过指针修改所指内容。

  5. 与 #define 的区别:const 定义的常量有类型,可以进行类型检查;而 #define 定义的常量是简单的文本替换,没有类型信息。

2.2 代码示例

示例1:基本常量

#include <stdio.h>  
  
int main() {  
    const int MAX_VALUE = 100; // 定义常量 MAX_VALUE  
    printf("MAX_VALUE: %d\n", MAX_VALUE);  
    // MAX_VALUE = 200; // 这将导致编译错误,因为 MAX_VALUE 是常量  
    return 0;  
}

运行结果:

示例2:指针指向常量

#include <stdio.h>  
  
int main() {  
    const int a = 5;  
    int b = 10;  
    const int *ptr1 = &a; // ptr1 指向常量 a,不能通过 ptr1 修改 a 的值  
    int *ptr2 = &b;       // ptr2 指向变量 b,可以通过 ptr2 修改 b 的值  
    // *ptr1 = 20;        // 这将导致编译错误,因为 ptr1 指向的是常量  
    *ptr2 = 20;           // 这将修改 b 的值为 20  
    printf("a: %d, b: %d\n", a, b);  
    return 0;  
}

运行结果:

示例3:指针本身为常量

#include <stdio.h>  
  
int main() {  
    int a = 5;  
    const int *ptr = &a; // ptr 指向变量 a,但 ptr 被声明为指向 const,因此不能通过 ptr 修改 a 的值  
    // ptr = &b;         // 假设 int b; 已声明,这将是合法的,但前提是 ptr 没有被声明为指向 const 的 const 指针  
    // *ptr = 10;        // 这将导致编译错误,因为 ptr 指向的值被视为常量  
    printf("a: %d\n", a);  
    return 0;  
}

运行结果:

示例4:指针指向常量且本身为常量

#include <stdio.h>  
  
int main() {  
    const int a = 5;  
    const int *const ptr = &a; // ptr 是指向 const 的 const 指针,既不能改变 ptr 的指向,也不能通过 ptr 修改所指向的值  
    // ptr = &b;               // 这将导致编译错误,因为 ptr 是指向 const 的 const 指针  
    // *ptr = 10;             // 这也将导致编译错误,因为 ptr 指向的值被视为常量  
    printf("a: %d\n", a);  
    return 0;  
}

运行结果:

通过 const 关键字可明确程序中不可修改的变量,减少意外修改导致的 bug,提升代码健壮性。

三、signed 和 unsigned

signed 和 unsigned 关键字用于定义整数类型的符号性,signed 表示有符号数,可表示正数、负数和零;unsigned 表示无符号数,仅能表示非负数(零和正数)。

3.1 作用

  1. 有符号数(signed):

    • 默认情况下,整数类型(如 int、short、long)都是有符号的。

    • 有符号数使用最高位作为符号位,0 表示正数,1 表示负数。

    • 有符号数的取值范围包括负数、零和正数。

  2. 无符号数(unsigned):

    • 无符号数不使用符号位,因此可以表示更大的正数范围。

    • 无符号数的取值范围从0开始,一直到该类型能表示的最大正数。

    • 在声明变量时,可以使用 unsigned 关键字来指定无符号类型,如 unsigned int、unsigned short、unsigned long 等。

  3. 类型转换:

    • 当有符号数和无符号数进行运算时,有符号数可能会被隐式转换为无符号数,可能会导致意外的结果。

    • 为了避免这种情况,应该显式地进行类型转换,确保运算的正确性。

  4. 溢出:

    • 当整数超出其类型的取值范围时,会发生溢出。

    • 对于有符号数,溢出可能导致结果变为负数或另一个正数。

    • 对于无符号数,溢出会导致结果从最大值回绕到0。

3.2 代码示例

示例1:基本的有符号和无符号整数

#include <stdio.h>  
  
int main() {  
    signed int a = -10;       // 有符号整数,值为-10  
    unsigned int b = 20;      // 无符号整数,值为20  
  
    printf("Signed int a: %d\n", a);  
    printf("Unsigned int b: %u\n", b);  
  
    // 有符号和无符号整数相加(注意可能的溢出和类型转换)  
    int sum_signed = a + b;   // 结果为10(有符号运算)  
    unsigned int sum_unsigned = a + b; // 结果取决于系统,但通常为一个大正数(无符号运算)  
  
    printf("Sum (signed): %d\n", sum_signed);  
    printf("Sum (unsigned): %u\n", sum_unsigned);  
  
    return 0;  
}

运行结果:

示例2:类型转换和溢出

#include <stdio.h>  
#include <limits.h>  
  
int main() {  
    unsigned int u_max = UINT_MAX; // 无符号整数的最大值  
    int s_max = INT_MAX;           // 有符号整数的最大值  
  
    printf("Unsigned int max: %u\n", u_max);  
    printf("Signed int max: %d\n", s_max);  
  
    // 溢出示例  
    unsigned int u_overflow = u_max + 1; // 结果为0(无符号溢出)  
    int s_overflow = s_max + 1;          // 结果为INT_MIN(有符号溢出)  
  
    printf("Unsigned overflow: %u\n", u_overflow);  
    printf("Signed overflow: %d\n", s_overflow);  
  
    // 类型转换示例  
    unsigned int u = 10;  
    int s = -5;  
  
    // 当有符号数和无符号数进行运算时,有符号数可能会被隐式转换为无符号数  
    unsigned int result = u + s; // 结果可能不是预期的5,而是一个大正数  
  
    printf("Result of u + s (unsigned): %u\n", result);  
  
    // 为了避免这种情况,应该显式地进行类型转换  
    unsigned int result_correct = u + (unsigned int)s; // 仍然可能不是5(因为s是负数),但避免了隐式转换的陷阱  
    int result_signed = (int)u + s; // 正确的结果为5(因为先将u转换为有符号数,再进行运算)  
  
    printf("Corrected result (unsigned to signed): %u\n", result_correct);  
    printf("Corrected result (signed): %d\n", result_signed);  
  
    return 0;  
}

运行结果:

使用有符号和无符号整数时,需明确场景需求,避免隐式转换和溢出导致的异常,必要时显式声明符号性和类型转换。

四、struct、union、enum

struct、union 和 enum 是用于定义复合数据类型的关键字,允许将多个不同类型数据组合或定义命名整型常量集合,适配复杂数据表示需求。

4.1 struct(结构体)

  • 结构体定义使用 struct 关键字,后跟结构体标签(可选)和大括号内的成员列表。

  • 结构体成员可以是任何有效的数据类型,包括基本数据类型、指针、数组、甚至其他结构体。

  • 结构体变量可以通过点运算符(.)访问其成员。

  • 结构体可以嵌套定义,即一个结构体成员可以是另一个结构体类型。

代码示例:

#include <stdio.h>  
  
// 定义一个结构体类型 Person  
struct Person {  
    char name[50];  
    int age;  
    float height;  
};  
  
int main() {  
    // 创建一个结构体变量  
    struct Person person1;  
  
    // 给结构体成员赋值  
    snprintf(person1.name, sizeof(person1.name), "Alice");  
    person1.age = 30;  
    person1.height = 5.5;  
  
    // 打印结构体成员的值  
    printf("Name: %s\n", person1.name);  
    printf("Age: %d\n", person1.age);  
    printf("Height: %.1f\n", person1.height);  
  
    return 0;  
}

运行结果:

4.2 union(联合体)

union 关键字用于定义联合体,它是一种特殊的数据结构,允许在相同的内存位置存储不同的数据类型。联合体的大小等于其最大成员的大小,且所有成员共享同一块内存。

①作用

  • 联合体定义使用 union 关键字,后跟联合体标签(可选)和大括号内的成员列表。

  • 联合体成员可以是任何有效的数据类型。

  • 联合体变量通过点运算符(.)访问其成员,但一次只能存储一个成员的值,因为所有成员共享内存。

  • 联合体通常用于节省内存或实现多态。

代码示例:

#include <stdio.h>  
  
// 定义一个联合体类型 Data  
union Data {  
    int i;  
    float f;  
    char str[20];  
};  
  
int main() {  
    // 创建一个联合体变量  
    union Data data;  
  
    // 给联合体成员赋值(注意:同时只能有一个成员有效)  
    data.i = 100;  
    printf("Integer: %d\n", data.i);  
  
    data.f = 3.14;  
    printf("Float: %.2f\n", data.f);  
  
    snprintf(data.str, sizeof(data.str), "Hello");  
    printf("String: %s\n", data.str);  
  
    // 注意:同时访问多个成员可能会导致未定义行为  
    // 例如:printf("Integer after string: %d\n", data.i); // 未定义行为  
  
    return 0;  
}

运行结果:

4.3 enum(枚举类型)

enum 关键字用于定义枚举类型,它是一种用户定义的类型,由一组命名的整型常量组成。枚举类型使得代码更加清晰易读,并限制了变量的取值范围。

①作用

  • 枚举定义使用 enum 关键字,后跟枚举标签(必须)和大括号内的枚举成员列表。

  • 枚举成员可以是任何有效的标识符,它们自动被赋予一个整型值,从0开始递增(除非显式指定)。

  • 枚举变量可以通过赋值或使用枚举成员来初始化。

  • 枚举类型通常用于表示一组相关的常量,如颜色、状态等。

②代码示例

#include <stdio.h>  
  
// 定义一个枚举类型 Color  
enum Color {  
    RED,  
    GREEN,  
    BLUE,  
    YELLOW = 3, // 显式赋值  
    PURPLE    // 自动赋值为4(因为YELLOW=3,所以PURPLE=4)  
};  
  
int main() {  
    // 创建一个枚举变量  
    enum Color favoriteColor = GREEN;  
  
    // 打印枚举变量的值(注意:打印的是整型值)  
    printf("Favorite color: %d\n", favoriteColor);  
  
    // 使用枚举成员进行比较  
    if (favoriteColor == RED) {  
        printf("You like red!\n");  
    } else if (favoriteColor == GREEN) {  
        printf("You like green!\n");  
    } else {  
        printf("You like some other color.\n");  
    }  
  
    return 0;  
}

运行结果:

struct、union、enum 为 C 语言提供了灵活的复合数据类型支持,分别适配多属性组合、内存共享、常量集合等场景,是构建复杂程序的基础。

五、typedef

typedef 是 C/C++ 语言中的一个关键字,它允许程序员为现有的数据类型定义一个新的名称(别名)。这样做的好处是,它可以使代码更加清晰易读,特别是当处理复杂的数据类型(如结构体、联合体、指针等)时。

5.1 作用

  • typedef 的基本语法是 typedef existing_type new_type_name;,其中 existing_type 是已经存在的数据类型,new_type_name 是想要定义的新名称。

  • 使用 typedef 定义的别名,就像使用任何基本数据类型一样,可以用于变量声明、函数参数、返回值类型等。

  • typedef 经常与结构体(struct)和联合体(union)一起使用,以简化对这些复合数据类型的引用。

  • 还可以为指针类型定义别名,这在处理函数指针和复杂数据结构时特别有用。

与 define 的核心区别:

5.2 代码示例

示例1:为结构体/联合体定义别名

#include <stdio.h>  
  
// 定义一个结构体类型  
struct Point {  
    int x;  
    int y;  
};  
  
// 使用 typedef 为结构体类型定义别名  
typedef struct Point Point;  
  
int main() {  
    // 使用别名创建结构体变量  
    Point p1;  
  
    // 给结构体成员赋值  
    p1.x = 10;  
    p1.y = 20;  
  
    // 打印结构体成员的值  
    printf("Point p1: (%d, %d)\n", p1.x, p1.y);  
  
    return 0;  
}

运行结果:

typedef struct Point Point; 实际上有点冗余,因为当 struct 标签(Point)和 typedef 定义的别名相同时,可以直接在 typedef 中定义结构体,如下所示:

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

示例2:为指针和函数指针定义别名

#include <stdio.h>  
  
// 定义一个函数类型  
typedef int (*FuncPtr)(int, int);  
  
// 定义一个函数,该函数符合 FuncPtr 类型的签名  
int add(int a, int b) {  
    return a + b;  
}  
  
int main() {  
    // 使用别名创建函数指针变量  
    FuncPtr fp = add;  
  
    // 通过函数指针调用函数  
    int result = fp(3, 4);  
  
    // 打印结果  
    printf("Result: %d\n", result);  
  
    return 0;  
}

运行结果:

typedef 不创建新类型,仅为已有类型提供别名,合理使用可大幅简化复杂类型的声明,让代码更简洁易维护,尤其在大型项目中能提升代码一致性。

六、随堂测试

题目sizeof 在编译时和运行时有何区别?请举例说明 sizeof 对数组和指针操作的结果差异。(常见于各大公司C语言笔试/面试题)

答案

sizeof 是编译时一元运算符(非函数),其值在编译阶段就已确定,不会增加运行时开销。

差异示例:在函数内部,对于数组 char arr[10]sizeof(arr) 返回整个数组的大小10;而对于指针 char *p = arrsizeof(p) 返回的是指针变量本身的大小(如4或8字节),与其指向的数据大小无关。

题目:解释C语言中 const int *pint *const pconst int *const p 的区别。(《C陷阱与缺陷》经典题型,面试高频题)

答案

  • const int *p:指向常量的指针。指针指向的int数据是常量,不能通过p修改该数据,但p可以指向别的地址。

  • int *const p:常量指针。指针本身是常量,一旦初始化后就不能再指向其他地址,但可以通过p修改其指向的数据。

  • const int *const p:指向常量的常量指针。两者皆不可变,既不能修改指针的指向,也不能通过指针修改其指向的数据。

题目:简述结构体(struct)的内存对齐原则。为什么需要内存对齐?(腾讯、华为等公司嵌入式开发岗位面试真题)

答案

原则主要包括:

  1. 结构体成员的起始地址偏移量必须是其自身大小或对齐模数的整数倍;

  2. 结构体总大小是其最宽基本类型成员大小的整数倍。

原因:内存对齐是硬件要求,CPU访问对齐的内存速度更快;未对齐的访问在某些架构上会导致硬件异常或性能损失。

问题(字节跳动 2023 校招 C 语言面试题):在 32 位系统中,计算以下结构体的大小:

struct Test {
    char a;
    int b;
    short c;
};

答案:12 字节。

解析:内存对齐规则,char (1 字节) 补 3 字节→4 字节,int (4 字节)→8 字节,short (2 字节) 补 2 字节→12 字节,总大小为最大成员(int,4 字节)的整数倍。

问题(腾讯 2022 校招 C 语言面试题):简述 C 语言中 typedef 与 #define 的核心区别(至少 2 点)。

答案

①typedef 有类型检查,#define 仅文本替换无类型校验;

②typedef 作用域受声明位置限制(如局部作用域),#define 全局有效(除非 #undef);

③typedef 可定义复杂类型别名(如函数指针),#define 无法直接实现。

问题:请区分 const int* p、int* const p、const int* const p 的差异。

答案

①const int* p:指向常量的指针,不能通过 p 修改所指值,可改指针指向;

②int* const p:指针本身是常量,不能改指向,可修改所指值(非 const 时);

③const int* const p:既不能改指向,也不能通过 p 修改所指值。

题目sizeofstrlen有什么区别?(百度、腾讯等公司C语言基础面试高频题)

答案

  • sizeof运算符,在编译时计算操作数所占用内存的字节数,操作数可以是类型或变量。计算数组时得到数组总大小。

  • strlen库函数,在运行时计算以空字符\0结尾的字符串的字符长度(不包括\0)。传入的必须是字符指针(const char*)。

题目:请解释 const int *pint *const pconst int *const p的含义。(华为、中兴等嵌入式岗位面试真题)

答案

  • const int *p指向常量的指针。可以改变 p的指向,但不能通过 p修改其所指向的值。

  • int *const p常量指针。一旦初始化,p的指向不能再改变,但可以通过 p修改其所指向的值。

  • const int *const p指向常量的常量指针。既不能改变 p的指向,也不能通过 p修改其所指向的值。

题目struct(结构体)和 union(联合体)在内存布局上最根本的区别是什么?(英特尔、联发科等硬件相关公司面试题)

答案:最根本的区别在于内存****使用方式

  • 结构体:各成员拥有独立的内存空间,结构体总大小至少为所有成员大小之和(可能存在内存对齐填充)。

  • 联合体:所有成员共享同一段内存空间,联合体的大小由其最大成员决定,任一时刻只能有一个成员有效。