Trinity C programming Slide 5,6,7

107 阅读19分钟

trinity slide 5, 6,7

0. 八进制数

在C语言中,int a = 013; 表示的是一个八进制的整数。

在C语言中,如果一个整数以0开头(如013),那么这个数被解释为八进制数,而不是十进制数。因此:

  • 013是一个八进制数,等于十进制数11
  • 换句话说,int a = 013; 的含义是将十进制的11赋值给变量a

因此,int a = 013; 实际上是将十进制数11赋给变量a

1. C语言的指针大小

在C语言中,指针的大小取决于系统的架构(如32位或64位)和编译器。通常:

  • 在32位系统上,指针的大小为4字节(32位),因为地址空间的宽度是32位。
  • 在64位系统上,指针的大小为8字节(64位),因为地址空间的宽度是64位。

例如:

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

在32位系统上运行时,输出会显示4 bytes;而在64位系统上,输出会显示8 bytes

指针的大小是固定的,与它所指向的数据类型(如intcharfloat等)无关。

2. int const 和 const int

在C语言和C++中,const int aint const a 是等价的,它们都表示一个常量整数 a,也就是说a的值一旦被初始化就不能再修改。这两种写法的区别主要是语法层面上的,实际效果是相同的。

2.1 const int aint const a

  • const int a;a是一个常量整数。
  • int const a;a是一个常量整数。

这两种方式定义的变量a都是只读的,不能再被赋值。例如,a = 10; 是非法的。

2.2 为什么会有这两种写法?

  • C语言和C++中const的位置较灵活,可以放在数据类型前面或后面。
  • 这两种写法没有本质区别,通常选择哪种方式主要是个人习惯或团队风格的选择。

2.3 const的其他用法

在定义指针时,const的位置可能会影响含义。这时需要特别注意:

int const *p;    // p是指向常量整数的指针,整数不可修改,但指针可以
int *const p;    // p是一个常量指针,指针不可修改,但整数可以
const int *const p; // p是指向常量整数的常量指针,整数和指针都不可修改

在这些情况下,const的位置会影响到指针或数据的只读性,因此需要小心使用。

3. int x[]

在C语言中,通过 int x[]; 这种形式定义数组是不完整的,因为缺少数组大小的定义。为了正确地定义数组,必须在声明数组时指定其大小,或者在定义时直接对数组进行初始化。

有两种常见的使用场景可以实现int x[]这种无大小声明的写法:

3.1 初始化时自动推断大小

如果在声明时直接对数组进行初始化,编译器会自动推断数组的大小:实际上是C89就有的自动推断

int x[] = {1, 2, 3, 4}; // 自动推断大小为4

在这种情况下,编译器根据初始值的个数确定数组的大小,这样 x 会被定义为一个大小为 4 的整数数组。

3.2 函数形参中的不定长数组

当数组作为函数的形参传递时,可以使用 int x[] 来定义形参而不指定数组的大小:

void printArray(int x[], int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", x[i]);
    }
    printf("\n");
}

在函数参数中,int x[]int *x等效,表示一个指向整数类型的指针。因此在函数参数中,无需指定数组的具体大小。

注意事项

  • 如果只是声明一个局部数组或全局数组,不提供大小没有初始化会导致编译错误。
  • 在函数参数中不指定大小是允许的,因为数组会退化为指针。

在C语言中,初始化时自动推断数组大小这一特性是从C89标准(也称为ANSI C标准)就已经支持的,因此是C语言的一项早期特性。

解释

在C89及其后的标准(如C99、C11)中,允许通过初始化列表直接定义数组而无需显式指定大小,编译器会根据初始化列表的元素个数自动确定数组的大小。例如:

int x[] = {1, 2, 3, 4}; // 自动推断为大小为4的数组

这一特性极大地简化了代码编写,使得开发者可以更直观地定义和初始化数组。


在C语言中,这种二维数组的初始化也是从C89标准(ANSI C)开始就支持的。因此,二维数组的初始化特性和一维数组类似,在C89及其后的标准(如C99、C11)中都支持 在定义时直接初始化数组的所有元素

示例解析

int A[][2] = {{1, 2}, {3, 4}, {5, 6}};

这里定义了一个二维数组 A,它有3行2列,每个元素都是直接初始化的:

  • 第一行初始化为 {1, 2}
  • 第二行初始化为 {3, 4}
  • 第三行初始化为 {5, 6}

在这种情况下,编译器会根据初始化列表的内容自动推断数组的行数(3行),但列数 [2] 需要显式指定。即在二维数组中,如果省略了第一维的大小,编译器会根据初始值的数量推断行数。

#include <stdio.h>void printfirst(int C[][2]) {
    printf("First element = %d\n", C[0][0]);
    printf("Last element = %d\n", C[2][1]);
}
​
int main(void) {
    int A[][2] = {{1, 2}, {3, 4}, {5, 6}};
    printfirst(A);
    return 0;
}

输出结果

运行这段代码后,将输出:

First element = 1
Last element = 6

这段代码展示了如何定义和初始化一个二维数组,并通过传递给函数访问其中的元素。

3. 数组在参数中的退化现象(decay)

#include <stdio.h>void array_size_wrong(int A[]) {
    printf("In Function: Size = %ld\n", sizeof(A));
    for (int i = 0; i < 10; i++) {
        printf("%d ", A[i]);
    }
    printf("\n");
}
​
int main(void) {
    int X[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
​
    printf("In Main 1: Size = %ld\n", sizeof(X));
    array_size_wrong(X);
    printf("In Main 2: Size = %ld\n", sizeof(X));
​
    return 0;
}

数组在函数参数中的退化(decay)现象,以及使用 sizeof 操作符时可能遇到的误解。具体来说,图片展示了在C语言中,当数组作为函数参数传递时,数组会退化为指针,因此 sizeof 操作符在函数中得到的是指针的大小,而不是数组的实际大小。

具体内容解释

  1. 代码部分:图片中的代码包含一个名为 array_size_wrong 的函数和 main 函数。

    • main 函数中,定义了一个整数数组 X,并将其大小通过 sizeof(X) 输出,这里 X 的大小是 40字节(假设 int 是4字节,X 有10个元素)。
    • 然后,X 被传递给 array_size_wrong 函数。
    • array_size_wrong 函数中,使用 sizeof(A) 来获取数组参数 A 的大小,但因为 A 退化为指针,所以 sizeof(A) 实际返回的是指针的大小(64位系统上是8字节,32位系统上是4字节)。
  2. 输出解释

    • main 函数中,sizeof(X) 输出的是40字节,表示整个数组的大小。
    • 传递给 array_size_wrong 后,sizeof(A) 输出的是8字节,因为此时 A 被视为指针而非完整数组。
  3. 概念总结

    • 数组退化:在C语言中,数组在作为函数参数传递时,会退化为指针,因此不能通过 sizeof 在函数中得到数组的实际大小。
    • 使用方法:如果需要在函数中知道数组的大小,建议在传参时另传一个参数,专门用于表示数组的长度。

4. C和C++中字符数组和字符串字面量区别

C和C++中字符数组和字符串字面量的区别,尤其是在C++中关于类型安全的问题。在C++中,字符串字面量被视为不可修改的常量,因此应该使用const限定符,而在C语言中则较为宽松。

  1. 字符串字面量的存储类型

    • 在C语言中,虽然字符串字面量(如"MSC")在大多数实现中存储在只读区域,但并不强制要求字符串字面量是不可修改的。
    • 在C++中,字符串字面量被定义为常量字符数组,因此被视为不可修改的。将字符串字面量赋给一个char*指针是潜在不安全的操作。
  2. 问题代码与警告

    char *B = "MSC";
    

    在C++中会导致编译器警告:

    warning: ISO C++ forbids converting a string constant tochar*’
    
    • 这条警告的原因是C++标准不允许将字符串常量直接赋给一个char*,因为字符串常量应该是只读的,而char*指针可以指向可修改的数据。
  3. 正确的写法

    • 为了遵循C++的类型安全,推荐使用const char *B = "MSC";,将字符串字面量赋给const char*,表示B指向的字符串内容是不可修改的。
    • const char *B = "MSC";的写法既符合C++的标准,也避免了警告或潜在的未定义行为。
  4. 总结

    • C语言中可以用char *B = "MSC";,虽然这在某些情况下可能会导致未定义行为(尝试修改字符串会出错),但通常不会导致编译错误。
    • C++中更强调类型安全,强制要求将字符串字面量赋给const char*,确保字符串不可修改。

结论

  • 在编写兼容C和C++的代码时,建议使用const char*来表示指向字符串字面量的指针,以提高代码的安全性和可读性。

5. Memory layout of Program

这个图描述了一个运行中的程序的内存布局,并分为几个主要的内存区域,每个区域存储不同类型的数据。以下是各个区域的详细解释:

从下到上

5.1 Text Segment(代码段)

  • 位置:内存布局的最底部。
  • 作用:存储程序的可执行代码。
  • 特点:通常是只读的,以防止程序的代码意外修改,提高安全性。

5.2. Data Segment(数据段)

  • 位置:位于代码段之上。

  • 作用:存储全局变量静态变量,这些变量在程序生命周期内始终存在。

  • 特点

    :数据段包含已初始化数据段和未初始化数据段。

    • 已初始化的数据(例如 int x = 5;)在程序启动时会被赋予指定的初始值。
    • 未初始化的数据(例如 static int y;)在程序启动时自动初始化为0。

5.3. Heap(堆)

  • 位置:数据段之上,向上增长。
  • 作用:用于动态内存分配。当程序需要在运行时分配内存(例如通过malloccalloc等),内存从堆中分配。
  • 特点:内存的分配和释放由程序员控制,不会自动回收,需要小心管理以避免内存泄漏。

5.4. Stack(栈)

  • 位置:在堆之上,向下增长。
  • 作用:存储自动变量(局部变量)和函数调用信息(如函数的返回地址、参数和局部变量)。
  • 特点:由编译器自动分配和释放。当函数调用结束时,栈上的内存自动释放。栈内存的分配速度快,但大小有限,递归或过深的嵌套可能会导致栈溢出。

5.5. Unmapped Area(未映射区域)

  • 位置:在栈的上方。
  • 作用:包含命令行参数环境变量等。该区域一般不分配给应用程序,作为内存空间的保留区域。
  • 特点:在程序启动时加载,包含了运行时需要的环境信息。

总结

  • 程序的内存布局按TextDataHeapStackUnmapped区域分层。
  • 各个区域存储不同类型的数据,满足程序的不同需求,例如代码、全局数据、动态分配内存、局部变量和环境信息等。
  • 这种内存布局有助于提高内存管理的效率,确保数据的安全性和程序运行的稳定性。

6. malloc的强制类型转换问题

在使用 malloc 时,是否需要进行强制类型转换取决于编程语言的要求,确实在 C 和 C++ 中有所不同。

6.1 在 C 中

C语言中,不需要进行强制类型转换malloc 函数返回一个 void* 类型的指针,表示可以指向任意类型的数据。在 C 语言中,void* 可以自动转换为其他指针类型,因此不需要显式的类型转换。

例如:

int *arr = malloc(10 * sizeof(int)); // 不需要强制转换

这样写是完全合法的,因为在 C 中,void* 自动转换为 int*

6.2 在 C++ 中

C++语言中,需要进行强制类型转换。这是因为 C++ 是一个更严格的类型检查语言,void* 不会自动转换为其他指针类型。为了使用 malloc 返回的指针,必须进行类型转换。

例如:

int *arr = (int*)malloc(10 * sizeof(int)); // 必须进行强制转换

在 C++ 中,推荐使用 newdelete 来进行内存管理,而不是 mallocfree,因为 newdelete 是类型安全的,并且会自动调用构造函数和析构函数。

6.3 什么时候需要强制类型转换?

总结来说:

  • 在 C 中:不需要强制类型转换,void* 自动转换。
  • 在 C++ 中:需要强制类型转换,或者优先使用 newdelete

6.4 为什么有这种区别?

  • C++ 更注重类型安全,所以限制更严格。
  • C 语言强调简洁性和兼容性,允许 void* 的隐式转换,以适应动态内存管理。

7. perror / strerror

perror 是 C 标准库中的一个函数,用于打印错误信息。它通常在检测到系统调用或库函数出错时使用,以便输出错误原因并便于调试。perror 函数会根据全局变量 errno 的值,输出对应的错误描述信息。

函数定义

#include <stdio.h>void perror(const char *s);

参数

  • s:一个字符串指针,通常表示错误发生的位置或上下文。perror 会先打印 s,然后打印一个冒号和空格,接着输出 errno 对应的错误信息。
  • 如果 sNULL 或为空字符串,perror 只输出错误信息部分。

perror 的工作原理

  • perror 使用全局变量 errno,该变量在系统调用或某些库函数出错时会被设置为对应的错误代码。
  • perror 通过 errno 查找错误代码对应的错误描述信息(通常是一个字符串),并将 s 和错误描述信息一起打印到标准错误流(stderr)。

示例

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>int main() {
    FILE *fp = fopen("nonexistent.txt", "r");
    if (fp == NULL) {
        perror("Error opening file");
        return EXIT_FAILURE;
    }
    fclose(fp);
    return EXIT_SUCCESS;
}

EXIT_SUCCESS:通常等于 0,表示程序成功执行并正常退出。

EXIT_FAILURE:通常等于非零值(例如 1),表示程序遇到错误并异常退出。

输出

假设文件 nonexistent.txt 不存在,那么运行这段代码将输出:

Error opening file: No such file or directory
  • perror 输出 Error opening file,然后是冒号和空格,接着输出 No such file or directory,这是 errno 设置为 ENOENT(表示文件不存在)时的错误信息。

适用场景

  • 系统调用或标准库函数失败时:例如文件打开失败(fopen)、内存分配失败(malloc)、进程创建失败(fork)等。
  • 调试和日志记录perror 可以帮助开发者快速找到错误原因,便于调试和定位问题。

perrorstrerror 的区别

  • strerror
    

    :返回

    errno
    

    对应的错误信息字符串,而不直接输出。这在需要对错误信息进一步处理时更有用。

    printf("Error: %s\n", strerror(errno))
    
  • perror:直接将错误信息输出到标准错误流 stderr

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>int main() {
    FILE *file = fopen("nonexistent.txt", "r");
​
    if (file == NULL) {
        // 打印自定义错误消息以及 strerror(errno) 返回的错误描述
        printf("Error opening file: %s\n", strerror(errno));
        return EXIT_FAILURE;
    }
​
    // 如果文件打开成功,关闭文件并退出
    fclose(file);
    return EXIT_SUCCESS;
}
​

总结

  • perror 是用于打印错误信息的便捷工具,特别是在系统调用和库函数出错时。
  • 通过 perror,可以更清晰地了解错误的原因,提高代码的可读性和可调试性。

8. 计算结构体大小

在C语言中,结构体的大小是通过结构体中各个成员的大小以及它们在内存中的对齐方式来计算的。以下是一些影响结构体大小的关键因素:

8.1. 基本规则

  • 结构体的大小是所有成员大小的总和,考虑了对齐填充。
  • 编译器可能会在成员之间或结构体末尾添加填充字节(padding),以满足平台的内存对齐要求。
  • 结构体的大小通常是其最大成员对齐要求的倍数,以确保在数组中连续结构体元素的对齐。

8.2 内存对齐和填充

  • 在内存中访问数据时,CPU更快地访问对齐的地址。因此,编译器会根据系统的对齐要求调整结构体成员的位置。
  • 一般来说,int 类型需要4字节对齐,double 类型需要8字节对齐等等。
  • 如果某个成员需要4字节对齐,而前一个成员不是4的倍数,则编译器会插入填充字节。

8.3 示例代码

以下是一个简单的结构体例子,演示如何计算结构体的大小:

#include <stdio.h>struct Example {
    char a;     // 1字节
    int b;      // 4字节
    char c;     // 1字节
};
​
int main() {
    printf("Size of struct Example: %lu\n", sizeof(struct Example));
    return 0;
}

分析

  1. char a 占用 1 字节。
  2. int b 需要 4 字节,并且 4 字节对齐,因此编译器在 a 后面填充了 3 个字节,使 b 对齐。
  3. char c 占用 1 字节,后面通常会有3个字节的填充,使整个结构体的大小成为 8 的倍数(在32位和64位系统中通常为8字节对齐)。

结构体布局

结构体 Example 的布局如下:

| char a | padding (3 bytes) | int b | char c | padding (3 bytes) |

计算结果

  • sizeof(struct Example) 的值通常是 12 字节,因为编译器添加了填充字节来满足对齐要求。

8.4 实际计算

假设系统上 int 为 4 字节对齐:

  • a 为 1 字节,加上 3 字节填充,使得 b 从第 4 字节开始。
  • b 为 4 字节,占据第 4 到第 7 字节。
  • c 为 1 字节,再次加上 3 字节填充,使得结构体大小为 12 字节。

8.5 避免填充的技巧

可以通过调整成员的顺序来减少填充字节。例如,将 int b 放在 char a 前面:

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

在这种情况下,结构体可能只占用 8 字节(具体取决于编译器的对齐方式),因为没有多余的填充。

总结

  • 结构体大小 = 各成员的大小 + 填充字节。
  • 编译器根据对齐规则添加填充字节,使成员在内存中对齐。
  • sizeof 操作符可以确定结构体的实际大小。

8.6 修改对齐数

在C语言中,可以通过编译器指令编译器特性来更改结构体的对齐数。具体方法取决于编译器,比如 GCCMSVC 提供了不同的方式来控制对齐。

  1. 使用 #pragma pack(适用于 MSVC 和 GCC)

GCCMSVC 中,可以使用 #pragma pack 指令来修改结构体的对齐方式。#pragma pack 指令指定编译器以指定字节数对齐结构体成员,从而减少或增加结构体的内存占用。

#include <stdio.h>#pragma pack(1) // 设置对齐数为 1 字节
struct Example {
    char a;
    int b;
    char c;
};
#pragma pack() // 恢复默认对齐方式int main() {
    printf("Size of struct Example: %lu\n", sizeof(struct Example));
    return 0;
}

解释

  • #pragma pack(1) 将结构体的对齐数设置为 1 字节,这意味着结构体中的每个成员将按照 1 字节边界对齐。
  • 上面的代码将 struct Example 的大小缩小为没有填充字节的大小,即 1 + 4 + 1 = 6 字节。
  • #pragma pack() 不带参数时,恢复到默认的对齐方式。
  1. 使用 __attribute__((packed))(适用于 GCC)

GCC 中,还可以使用 __attribute__((packed)) 指定结构体为紧凑排列,无填充字节。

#include <stdio.h>struct Example {
    char a;
    int b;
    char c;
} __attribute__((packed)); // 紧凑对齐int main() {
    printf("Size of struct Example: %lu\n", sizeof(struct Example));
    return 0;
}

解释

  • __attribute__((packed)) 强制编译器不为结构体成员添加填充字节。
  • 在这种情况下,struct Example 的大小也是 1 + 4 + 1 = 6 字节。
  1. 使用 __declspec(align(#))(适用于 MSVC)

MSVC 中,可以使用 __declspec(align(#)) 来设置结构体的对齐方式,# 可以是 1、2、4、8、16 等。__declspec(align(#)) 可以用来设置更大的对齐数,而 #pragma pack 主要用来设置更小的对齐数。

#include <stdio.h>
​
__declspec(align(8)) // 设置对齐数为 8 字节
struct Example {
    char a;
    int b;
    char c;
};
​
int main() {
    printf("Size of struct Example: %lu\n", sizeof(struct Example));
    return 0;
}

解释

  • __declspec(align(8)) 将结构体对齐数设置为 8 字节。这会导致结构体中的成员根据 8 字节对齐的规则排列,可能会有更多的填充字节。
  1. 注意事项
  • 更改对齐数可能会影响性能:过小的对齐数可能导致访问速度变慢,而过大的对齐数会浪费内存。
  • 编译器特定:这些指令是编译器特定的,不能保证跨平台兼容。如果要编写跨平台代码,应小心使用这些指令,并测试不同的对齐设置。
  • 对齐方式会影响结构体的大小:更改对齐方式后,结构体的大小可能会发生变化。通过 sizeof 可以验证。

总结

  • 可以使用 #pragma pack 指令(GCC 和 MSVC)或 __attribute__((packed))(GCC)来修改结构体的对齐方式。
  • 在 MSVC 中,还可以使用 __declspec(align(#)) 设置更大的对齐数。
  • 更改对齐方式应谨慎,过度调整可能会导致性能问题或浪费内存。