第六十三天学习记录:C语言个人总结

作者分享了系统学习C语言的经历,指出了指针、动态内存管理和函数栈帧等关键概念的重要性,并提及KMP算法和文件操作。在C语言学习后,计划同时学习C++和数据结构,认为两者相辅相成。文章还列举了学习资源和一些C语言关键字如inline、register、volatile的用法,并介绍了C99和C11标准的新特性。
摘要由CSDN通过智能技术生成

在花了差不多2个月的时间,完整的跟着一个视频课程将C语言较为系统的学习了一遍。尽管C语言在大学的时候为了考计算机二级等级考试而自学过,但十多年后的今天再次学习时却如初见。
通过这次学习可以说是收获颇多。
给我印象最深刻的是:
1、指针。
指针可以说是C/C++的核心。学习了C语言的指针甚至对我当前工作中主用Qt写程序的项目都帮助颇多。很多以前遇到的问题都迎刃而解。
2、动态内存管理。
这一块可以说如果单靠自学也是比较难以理解的。
3、函数栈帧。
这一块老师并没有详细讲解,理论上这个不属于C语言的范畴,但要是没有老师的引导,单单靠自己摸索也是很难理解起奥妙的。尽管通过查资料自学后对函数栈帧的理解也仅仅是皮毛,但至少已经知晓其工作原理。
4、KMP算法。这个也不属于C语言范畴,是老师推荐课后了解的。
5、文件操作。这一章节可以说是函数最多的一节,虽然用途没有指针、内存管理这些广泛,但并不代表着不重要。
总之,通过这两个月的学习,我已基本养成良好的习惯。希望这个习惯能够伴我终生。
对于学完C语言之后学什么?在纠结了1个小时之后,我决定C++和数据结构同时进行学习。
首先,这两门课程的方向不一样,不管先学哪一门对另一门的影响不是很大。但影响不大不代表着没有影响。在某一程度上,这两门课程可以说是相辅相成。听大佬分享学习经验说数据结构的进阶部分涉及到的内容C语言已经不能满足,必须得靠C++实现。而学习C++如果有数据结构的基础学起来会更有效,因此最终决定一起学习。

下面是总结在学习C语言课程中老师推荐的读物和刷题网站,不知道有没有遗漏但我想能把这些都掌握已经需要相当长的时间了。

学完C语言课程只能说是上道了,后面的巩固肯定要持续进行:

《高质量C/C++编程指南》中附录的考试试卷
《C语言深度解剖》
《程序员的自我修养》-连接、装载与库
《C陷阱和缺陷》
《剑指offer》
《C程序员编程艺术》
简明VIM练级攻略:
https://coolshell.cn/articles/5426.html
给程序员的VIM速查卡
https://coolshell.cn/articles/5479.html
牛客网刷题

在学完C语言课程后,仍然有些关键词很模式,于是总结了一波问ChatAI:

问:C语言inline

ChatAI答:
在 C 语言中,inline 是一种关键字,用于告知编译器对于函数的调用是否应该被内联。内联函数可以实现函数的调用处替换为函数本身的代码,这一过程可以减少程序运行时函数调用的开销,从而提高程序的运行速度。

使用 inline 声明的函数并不一定会被内联,具体取决于编译器的实现。编译器可能会根据该函数的复杂度、代码大小等因素来作出内联的决策。当然,我们也可以使用 -O3 等编译选项来告诉编译器尽可能地内联函数。

以下是一些使用 inline 的示例:

inline int add(int a, int b){
    return a + b;
}

在上述代码中,add 函数被声明为内联函数,以便在需要时可以被内联。

需要注意的是,在 C99 标准中,inline 的语义有所不同。在 C99 中,inline 更多地被认为是一种链接指令,它告诉编译器,这个函数可能在多个源文件中出现,但所有对该函数的调用都应该被替换为一个函数的实例,以便减少链接时的重复代码。

问:register在什么情况下使用,可否举个简单的例子?
ChatAI答:
register 是一种 C 语言的关键字,用于向编译器请求将变量存储在寄存器中,以实现更快的访问速度。使用 register 关键字并不会直接将变量保存在寄存器中,而只是向编译器提供一个提示,让它尽可能地将该变量存储在寄存器中。

register 关键字的使用是可选的,它通常用于在程序中频繁访问的变量上,例如循环计数器、指针等。但是在现代编译器中,通常会根据编译器的算法自动将变量存储在寄存器中,因此手动添加 register 关键字的效果有限,有时甚至可能会降低代码的性能。

下面是一个简单的 register 关键字使用的例子:

#include <stdio.h>

int main() {
    register int i; // 将 i 存储在寄存器中
    int sum = 0;

    for(i = 1; i <= 10000; i++) {
        sum += i;
    }

    printf("%d\n", sum);
    return 0;
}

在该例子中,循环计数器 i 被声明为 register 类型,这告诉编译器我们希望尽可能地将它保存在寄存器中,以实现更快的访问速度。但是,在大多数情况下,没有必要显式地将变量声明为 register,因为编译器通常会自动将常用的变量存储在寄存器中,并使用最佳的算法优化代码。

问:volatile在C语言编译的情况下,什么时候会用?可否举个简单的例子。
ChatAI答:
volatile 是 C 语言中的一个关键字,用于告诉编译器,某个变量的值可能在程序的不同地方被意外地更改。

通常情况下,编译器会根据自己的算法将变量保存在寄存器或者缓存中,以提高程序的执行效率。但是,在某些情况下,程序可能会意外地修改变量的值,这样会导致 cached 的变量值和实际的变量值不同步,可能会产生一些错误。这就是 volatile 关键字的作用,它会告诉编译器,某些变量每次访问时都应该从内存中读取,而不是从寄存器或者缓存中读取。

下面是一个简单的 volatile 关键字的例子:

#include <stdio.h>

int main() {
    volatile int x = 0; // 告诉编译器,变量 x 是 volatile 类型
    int i;

    for (i = 1; i <= 10; i++) {
        x = i;
        printf("%d\n", x);
    }

    return 0;
}

在该例子中,变量 x 被声明为 volatile 类型,这告诉编译器,在程序执行的过程中,变量 x 的值可能会被意外地更改,在每次访问 x 时都需要从内存中读取它的值。如果不使用 volatile 关键字,编译器可能会将变量保存在寄存器中,以提高程序的执行效率,但这可能会导致 printf 函数输出的结果不正确。

需要特别注意的是,volatile 关键字只用于告诉编译器,某些变量的值可能会被意外地更改,它并不保证线程安全。如果在多个线程中访问同一个 volatile 变量,仍然需要使用同步机制,以确保线程安全。

问:C99比较重要常用的特性。
ChatAI答:
C99引入了许多新特性,包括:

  1. 新的数据类型:增加了一个新的数据类型_Bool(布尔类型)。
  2. 可变长数组:可以在运行时分配数组大小,而不需要使用动态内存分配函数。
  3. 复合字面量:同时初始化多个结构或联合类型对象。
  4. 宏对函数的支持:允许使用宏定义实现内联函数,用于替代函数调用。
  5. 行注释://可以用于注释整行代码。
  6. 新的限定符:restrict 关键字可以用于提示编译器,指针是唯一的对象引用,可以用于优化某些代码。
  7. 改进的数学库:增加了许多新的数学函数,如计算双曲函数和复数函数等。

这些特性中,可变长数组和复合字面量是C99最重要的特性之一,使得C语言能够更加方便、简洁地处理数组和结构体类型的数据。同时,C99的改进还包括提高了可移植性、增加了类型安全和表达能力等方面的优化。

问:C语言的restrict

restrict 关键字是 C99 标准引入的一种类型修饰符。它用来告诉编译器,一个指针所指向的内存区域在一段程序段中是唯一被访问的。

使用 restrict 关键字后,编译器可以更好地进行代码优化,减少指针的加载和存储操作,从而提高代码的执行效率。

请注意,使用 restrict 关键字时需要遵守一些限制条件,例如不能使用多个指针同时访问同一内存区域等。此外,restrict 关键字的使用也需要考虑代码的可维护性,不要过度优化而使代码难以理解和修改。

当我们在 C 语言中使用指针时,编译器无法确定该指针所指向的内存区域是否会被其他指针所修改,从而影响程序的正确性和效率。这时我们可以使用 restrict 关键字明确告诉编译器一个指针是唯一的,并且不会受到其他指针的干扰。

以下是一个简单的例子,展示了如何在 C 语言中使用 restrict 关键字:

void array_add(int *restrict a, int *restrict b, int *restrict c, int n) {
    for (int i = 0; i < n; i++) {
        c[i] = a[i] + b[i];
    }
}

在这个例子中,我们定义了三个参数 abc,它们均为指向整型数组的指针。同时我们使用了 restrict 关键字,明确告诉编译器这三个指针所指向的数组是不同的,它们不会受到其他指针的干扰。

通过使用 restrict 关键字,编译器可以更好地进行代码优化,例如可以将 a[i]b[i] 的读操作合并为一个,减少内存访问次数,提高代码的执行效率。

问:C语言 _Bool 可否举个简单的例子。
_Bool 是 C99 标准中新增加的关键字,用于表示布尔类型。

以下是一个简单的例子,展示了如何在 C 语言中使用 _Bool 类型:

#include <stdio.h>
int main() {
    _Bool a = 1;
    _Bool b = 0;
    _Bool c = a && b;
    _Bool d = a || b;
    printf("a = %d, b = %d, c = %d, d = %d\n", a, b, c, d);
    return 0;
}

在这个例子中,我们首先定义了三个 _Bool 类型的变量 abc,分别赋值为 10a && b。接着我们又定义了一个 _Bool 类型的变量 d,赋值为 a || b。最后我们通过 printf 函数将这些变量的值输出。

运行输出的结果为:

a = 1, b = 0, c = 0, d = 1

可以看出,_Bool 类型的变量只能表示两种取值:01,其中 0 表示假(false),1 表示真(true),在逻辑运算符 &&|| 中按照布尔运算规则进行计算。

问:C语言 _Complex 可否举个简单的例子。
_Complex 是 C99 标准中新增加的关键字,用于表示复数类型。

以下是一个简单的例子,展示了如何在 C 语言中使用 _Complex 类型:

#include <stdio.h>
#include <complex.h>

int main() {
    double complex z1 = 1.0 + 2.0 * I;
    double complex z2 = 3.0 - 4.0 * I;
    double complex z3 = z1 + z2;
    double complex z4 = z1 * z2;
    double re = creal(z1);
    double im = cimag(z1);
    printf("z1 = %f + %fi\n", re, im);
    printf("z2 = %f + %fi\n", creal(z2), cimag(z2));
    printf("z3 = %f + %fi\n", creal(z3), cimag(z3));
    printf("z4 = %f + %fi\n", creal(z4), cimag(z4));
    return 0;
}

在这个例子中,我们首先使用 double complex 类型定义了两个复数变量 z1z2,分别赋值为 1.0 + 2.0 * I3.0 - 4.0 * I。然后我们分别计算了 z1 + z2z1 * z2 的结果,并将其赋值给 z3z4。接着我们使用 crealcimag 函数分别获取 z1 的实部和虚部,并将计算结果输出。

运行输出的结果为:

z1 = 1.000000 + 2.000000i
z2 = 3.000000 - 4.000000i
z3 = 4.000000 - 2.000000i
z4 = 11.000000 - 2.000000i

可以看出,_Complex 类型的变量可以进行常规的加、减、乘、除等数学运算操作,并且可以使用 crealcimag 函数分别获取复数的实部和虚部。

问:C语言 _Imaginary 可否举个简单的例子。

_Imaginary 是 C99 标准中新增加的关键字,用于表示虚数类型。

以下是一个简单的例子,展示了如何在 C 语言中使用 _Imaginary 类型:

#include <stdio.h>
#include <complex.h>

int main() {
    double _Imaginary i = 1.0i;
    double complex z1 = 1.0 + 2.0 * i;
    double complex z2 = 3.0 - 4.0 * i;
    double complex z3 = z1 + z2;
    double complex z4 = z1 * z2;
    double re = creal(z1);
    double im = cimag(z1);
    printf("z1 = %f + %fi\n", re, im);
    printf("z2 = %f + %fi\n", creal(z2), cimag(z2));
    printf("z3 = %f + %fi\n", creal(z3), cimag(z3));
    printf("z4 = %f + %fi\n", creal(z4), cimag(z4));
    return 0;
}

在这个例子中,我们首先使用 double _Imaginary 类型定义了一个虚数变量 i,并赋值为 1.0i。然后我们使用 double complex 类型定义了两个复数变量 z1z2,分别赋值为 1.0 + 2.0 * i3.0 - 4.0 * i。然后我们分别计算了 z1 + z2z1 * z2 的结果,并将其赋值给 z3z4。接着我们使用 crealcimag 函数分别获取 z1 的实部和虚部,并将计算结果输出。

运行输出的结果和上一个例子是一样的:

z1 = 1.000000 + 2.000000i
z2 = 3.000000 - 4.000000i
z3 = 4.000000 - 2.000000i
z4 = 11.000000 - 2.000000i

可以看出,_Imaginary 类型的变量可以和 _Complex 类型的变量一起进行运算,并且可以使用 crealcimag 函数分别获取虚数对应的复数的实部和虚部。

问:C语言的C11标准运用广泛吗?新增了什么重要且常用的特性。
相对于C99标准,C11标准的运用并不如广泛,但它也有其独特的特性和应用场景.

C11标准引入了一些新的特性,包括:

  1. 多线程支持:添加了一些新库函数和类型,如<threads.h>头文件中的线程管理函数等,使得C语言可以更方便地处理多线程应用程序的开发。
  2. Atomics类型:添加了对原子操作的支持,可以提供线程之间的同步和避免数据竞争等问题(如:std::atomic_t)。
  3. 语言增强:增加了_Static_assert表达式、一致得初始化、泛型选择表达式、宏变量类型检查等语言增强,使得开发人员可以更容易地编写安全、高效的代码。
  4. 直接在函数定义中可以编写函数属性(Function Attributes)和函数使用说明(Function Specifiers)。
  5. 改进的 Unicode 支持:更好的 Unicode 字符串常量支持
  6. 增加能表示二进制字面量。

总的来说,C11标准为C语言增加了一些新的特性和语言增强,如更好的多线程支持、原子操作支持和 Unicode 支持等,使得C语言可以更好地适应现代编程环境的需求,提高了C语言的效率、表达能力和可维护性等方面的优化。但是,由于C11标准的推广和应用较晚,还有很多程序员和开发者选择使用更为流行的C99标准。

问:C语言 _Alignas 可否举个简单的例子。
当我们定义一个变量时,通常情况下编译器会根据变量的类型和所在的位置来进行内存对齐,以提高读写效率。但在某些情况下,我们可能需要强制指定变量的内存对齐方式,这时就可以用到 C11 引入的 _Alignas 关键字。

例如,假设我们定义了一个结构体:

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

由于 int、char、short 分别占用 4、1、2 个字节,因此编译器会对这个结构体进行内存对齐,以 4 字节对齐。

现在假设我们要定义一个 char 数组,但是要求这个数组以 8 字节对齐,我们可以使用 _Alignas 来指定对齐方式:

_Alignas(8) char my_array[1024];

这样就能够保证 my_array 数组的起始地址是 8 的倍数,从而提高读写效率。

问:C语言 _Alignof 可否举个简单的例子。
_C11 标准中的 _Alignof 关键字用于返回一个类型的对齐方式(即其在内存中以几字节形式对齐)。

例如,我们定义了下面这个结构体:

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

编译器会对这个结构体进行内存对齐,以提高存储器读写速度。换句话说,编译器会根据这个结构体中的成员类型、大小、以及当前编译器的优化级别等因素,计算出一个最合适的内存对齐方式。我们可以使用 _Alignof 来查看这个结构体的对齐方式,如下所示:

#include <stdio.h>

int main() {
    printf("MyStruct is aligned to %zu bytes\n", _Alignof(struct MyStruct));
    return 0;
}

在上面的代码中,_Alignof(struct MyStruct) 返回的是结构体 MyStruct 对齐所需的字节数,由于这个结构体中包含了 int、char、short 等不同类型的成员,因此编译器会计算出一个最合适的对齐方式,输出的结果通常是 4 或 8,具体取决于不同的编译器和平台。

问:C语言 _Atomic 可否举个简单的例子。

_C11 标准中的 _Atomic 关键字可用于定义原子类型,原子类型是一种特殊的数据类型,在多线程程序中能够保证读写操作的原子性,从而避免了竞争条件的发生。

下面是一个简单的使用 _Atomic 定义原子类型的例子:

#include <stdio.h>
#include <stdatomic.h>
#include <pthread.h>

_Atomic int count = 0;

void* worker(void* arg) {
    for (int i = 0; i < 100000; i++) {
        count++;
    }
    return NULL;
}

int main() {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, worker, NULL);
    pthread_create(&t2, NULL, worker, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    printf("count = %d\n", count);
    return 0;
}

在该例子中,我们定义了一个 _Atomic int 类型的变量 count,并在两个线程中对该变量进行了加法操作。由于 count 是原子类型,因此每个线程对其进行的操作都是原子的,也就是说,在任何一个时刻,count 变量的值都是一个线程所对其进行的最后一次读写操作的结果,而不会因为线程之间的竞争而出现“竞态条件”。

需要指出的是,在多线程编程中使用原子类型能够减少每个线程的等待时间,从而提高了并发执行的效率。然而,过多地使用原子类型也会增加内存和 CPU 的开销,因此在实际编程中需要根据具体情况进行综合考虑。

问:C语言 _Static_assert 可否举个简单的例子。
C11 新增的 _Static_assert 关键字用于在编译期间进行静态断言,即在编译时检查一个表达式的值是否为真,并在表达式值为假时发出编译错误。它可用于确保代码的正确性,特别是对于一些底层编程中需要保证内存对齐和类型兼容性的情况。

以下是一个示例,它使用了 _Static_assert 来确保 char 类型的大小为 1:

#include <stdio.h>
#include <assert.h>

_Static_assert(sizeof(char) == 1, "Error: char is not one byte");

int main(void) {
    printf("char is one byte\n");
    return 0;
}

在上述代码中,我们使用 _Static_assert() 断言表达式 sizeof(char) == 1,如果不成立,编译器会在编译时报错并输出 “Error: char is not one byte”。

需要注意的是,_Static_assert 仅能在编译时生效,而无法在运行时进行检查。如果需要进行运行时检查,必须使用 assert() 函数。

_Noreturn
_C11 标准中的 _Noreturn 关键字用于告知编译器一个函数不会返回,这对于一些会使程序不可恢复或者出现未定义行为的函数十分有用。

使用 _Noreturn 关键字定义的函数不能返回到其调用位置,并且编译器在生成目标代码时会优化掉函数返回的代码,以减少生成的代码大小和提高代码效率。

下面是一个简单的例子:

#include <stdio.h>
#include <stdlib.h>
#include <stdnoreturn.h>

// 定义一个 _Noreturn 函数
_Noreturn void die(const char* message) {
    fprintf(stderr, "Fatal error: %s\n", message);
    exit(EXIT_FAILURE);
}

int main(void) {
    die("This is a test.");  // 调用 _Noreturn 函数
    printf("This is never executed.\n");
}

在上述代码中,我们定义了一个名为 “die” 的函数,它声明为 _Noreturn 类型,用于提示编译器该函数不会返回。在 main 函数中,我们调用了封装好的 die 函数,因为该函数不会返回,所以程序在执行完 “die” 函数后直接退出,printf 函数永远不会被执行。

需要注意的是,由于 _Noreturn 函数不会返回,因此在调用 _Noreturn 函数后执行的代码都将被编译器忽略。因此,在实际编程时必须保证在调用_Noreturn 函数之后的代码不会对程序的执行路径产生影响。

_Thread_local
_C11 标准中的 _Thread_local 关键字用于定义线程局部变量,即每个线程都有其自己的变量副本,且每个线程的变量副本之间互相独立、相互隔离,互不影响。

以下是一个简单的例子:

#include <stdio.h>
#include <pthread.h>

_Thread_local int count = 0;

void* worker(void* arg) {
    int i;
    for (i = 0; i < 10; i++) {
        count++;
    }
    printf("Thread %d: count=%d\n", *(int*)arg, count);
    return NULL;
}

int main() {
    pthread_t t1, t2;

    int thread1 = 1, thread2 = 2;

    pthread_create(&t1, NULL, worker, &thread1);
    pthread_create(&t2, NULL, worker, &thread2);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    return 0;
}

在上述代码中,我们定义了一个 _Thread_local 类型的变量 count,它用于保存每个线程自己的 count 值。每个线程都会对其自己的 count 变量进行操作,而不会影响到其他线程中的变量值。通过在不同的线程中打印 count 变量的值,可以发现每个线程都有其自己的 count 值:

Thread 1: count=10
Thread 2: count=10

需要注意的是,线程局部变量只在当前线程中有效,不能够共享或传递给其他线程,因为每个线程都有其自己的变量副本。这也就意味着,在对线程局部变量进行初始化时,该初始化语句必须放置在程序的每个线程入口点中,以保证每个线程都有其自己的变量副本。

_Generic
_C11 标准中的 _Generic 关键字用于编写针对不同数据类型的通用泛型代码。它可以根据传入的参数类型,选择不同的代码执行路径,从而避免写出大量的类型特定代码。

下面是一个简单的使用 _Generic 的例子:

#include <stdio.h>
#include <stdlib.h>

#define print(x) \
    _Generic((x), \
        int:    printf("print(int): %d\n", x), \
        float:  printf("print(float): %f\n", x), \
        double: printf("print(double): %f\n", x), \
        default: printf("print(default)\n") \
    )

int main() {
    int    i = 10;
    float  f = 10.10f;
    double d = 20.20;

    print(i);  // print(int): 10
    print(f);  // print(float): 10.100000
    print(d);  // print(double): 20.200000
    print("Hello, world!");  // print(default)

    return 0;
}

在上述示例中,我们定义了一个宏 “print”,并使用 _Generic 来识别宏参数的类型并输出相应类型的值。通过宏展开,我们可以针对不同类型的参数输出不同的信息。值得注意的是,在上述 _Generic 指令中,最后一个 default 关键字相当于一个通用的“else”分支,它会在无法匹配其他分支时执行。

_Generic 指令可以嵌套使用,可以根据不同的数据类型进行多层分支和选择,大大地增强了 C 语言的通用编程能力。但是,过多的 _Generic 指令可能会使代码变得复杂难懂,因此在实际编程时需要根据具体情况进行适当的选择。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值