在花了差不多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引入了许多新特性,包括:
- 新的数据类型:增加了一个新的数据类型_Bool(布尔类型)。
- 可变长数组:可以在运行时分配数组大小,而不需要使用动态内存分配函数。
- 复合字面量:同时初始化多个结构或联合类型对象。
- 宏对函数的支持:允许使用宏定义实现内联函数,用于替代函数调用。
- 行注释:
//
可以用于注释整行代码。 - 新的限定符:restrict 关键字可以用于提示编译器,指针是唯一的对象引用,可以用于优化某些代码。
- 改进的数学库:增加了许多新的数学函数,如计算双曲函数和复数函数等。
这些特性中,可变长数组和复合字面量是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];
}
}
在这个例子中,我们定义了三个参数 a
、b
和 c
,它们均为指向整型数组的指针。同时我们使用了 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
类型的变量 a
、b
和 c
,分别赋值为 1
、0
和 a && b
。接着我们又定义了一个 _Bool
类型的变量 d
,赋值为 a || b
。最后我们通过 printf 函数将这些变量的值输出。
运行输出的结果为:
a = 1, b = 0, c = 0, d = 1
可以看出,_Bool
类型的变量只能表示两种取值:0
和 1
,其中 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
类型定义了两个复数变量 z1
和 z2
,分别赋值为 1.0 + 2.0 * I
和 3.0 - 4.0 * I
。然后我们分别计算了 z1 + z2
和 z1 * z2
的结果,并将其赋值给 z3
和 z4
。接着我们使用 creal
和 cimag
函数分别获取 z1
的实部和虚部,并将计算结果输出。
运行输出的结果为:
z1 = 1.000000 + 2.000000i
z2 = 3.000000 - 4.000000i
z3 = 4.000000 - 2.000000i
z4 = 11.000000 - 2.000000i
可以看出,_Complex
类型的变量可以进行常规的加、减、乘、除等数学运算操作,并且可以使用 creal
和 cimag
函数分别获取复数的实部和虚部。
问: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
类型定义了两个复数变量 z1
和 z2
,分别赋值为 1.0 + 2.0 * i
和 3.0 - 4.0 * i
。然后我们分别计算了 z1 + z2
和 z1 * z2
的结果,并将其赋值给 z3
和 z4
。接着我们使用 creal
和 cimag
函数分别获取 z1
的实部和虚部,并将计算结果输出。
运行输出的结果和上一个例子是一样的:
z1 = 1.000000 + 2.000000i
z2 = 3.000000 - 4.000000i
z3 = 4.000000 - 2.000000i
z4 = 11.000000 - 2.000000i
可以看出,_Imaginary
类型的变量可以和 _Complex
类型的变量一起进行运算,并且可以使用 creal
和 cimag
函数分别获取虚数对应的复数的实部和虚部。
问:C语言的C11标准运用广泛吗?新增了什么重要且常用的特性。
相对于C99标准,C11标准的运用并不如广泛,但它也有其独特的特性和应用场景.
C11标准引入了一些新的特性,包括:
- 多线程支持:添加了一些新库函数和类型,如
<threads.h>
头文件中的线程管理函数等,使得C语言可以更方便地处理多线程应用程序的开发。 - Atomics类型:添加了对原子操作的支持,可以提供线程之间的同步和避免数据竞争等问题(如:std::atomic_t)。
- 语言增强:增加了_Static_assert表达式、一致得初始化、泛型选择表达式、宏变量类型检查等语言增强,使得开发人员可以更容易地编写安全、高效的代码。
- 直接在函数定义中可以编写函数属性(Function Attributes)和函数使用说明(Function Specifiers)。
- 改进的 Unicode 支持:更好的 Unicode 字符串常量支持
- 增加能表示二进制字面量。
总的来说,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 指令可能会使代码变得复杂难懂,因此在实际编程时需要根据具体情况进行适当的选择。