如何快速学习 C 语言 ?

大家好,我是小康,今天我们来聊下如何学习 C 语言。

C 语言,强大而灵活,是许多现代编程语言的基石。本文将带你快速了解 C语言 的基础知识,无论你是编程新手还是希望回顾基础,这里都有你需要的。

初学者在开始学习 C 语言的时候,往往不知道怎样高效的学习这门课,网上很多人都会推荐去看各种 C 语言书籍,我觉得没必要去看那么多,贪多嚼不烂!为了让更多初学的朋友快速入门 C 语言,我这里将 C 的各个知识点进行了汇总,并附有代码示例,以便大家理解,掌握这些就可以啦。如果你时间比较充足,可以看丹尼斯·里奇的《C程序设计语言》 这本书,再搭配浙大翁恺的 C 语言课程:C语言程序设计 浙江大学:翁恺_哔哩哔哩_bilibili

最佳的学习方法就是:根据我的知识点来看丹尼斯·里奇的《C程序设计语言》,再加上翁恺的 C 语言课程,搭配学习,效果最好。如果你认为自己自学能力很好或者时间有限,那么完全不需要看视频,本篇文章已经囊括了全部的知识点。

废话不多说了,直接带你快速入门 C 语言编程。

基础语法

1.标识符和关键字

标识符用于变量、函数的名称。规则:由字母、数字、下划线组成,但不以数字开头。

关键字是 C 语言中已定义的特殊单词,如 int、return 等。

示例:
int variable; // 'int' 是关键字, 'variable' 是标识符

2.变量和常量

变量是可以改变值的标识符。

常量是一旦定义,其值不可改变的标识符。常量使用 const来定义

示例:
int age = 25;       // 定义变量
const int MAX_AGE = 99; // 定义常量

3.运算符和表达式

在 C 语言中,运算符是用于执行特定数学和逻辑计算的符号。运算符可以根据它们的功能和操作数的数量被分为几个不同的类别。

算术运算符

这些运算符用于执行基本的数学计算。

+ 加法
- 减法
* 乘法
/ 除法
% 取余(模运算,只适用于整数)
关系运算符

关系运算符用于比较两个值之间的大小关系。

== 等于
!= 不等于
> 大于
< 小于
>= 大于等于
<= 小于等于
逻辑运算符

逻辑运算符用于连接多个条件(布尔值)。

&& 逻辑与
|| 逻辑或
! 逻辑非
赋值运算符

赋值运算符用于将值分配给变量。

= 简单赋值
+= 加后赋值
-= 减后赋值
*= 乘后赋值
/= 除后赋值
%= 取余后赋值
位运算符

位运算符对整数的二进制表示进行操作。

& 位与
| 位或
^ 位异或
~ 位非
<< 左移
>> 右移
递增和递减运算符

这些运算符用于增加或减少变量的值。此类运算符都只作用于一个操作数,因此也被称之为一元运算符

++ 递增运算符
-- 递减运算符
条件运算符

C 语言提供了一个三元运算符用于基于条件选择两个值之一。

? : 条件运算符

下面是一些使用这些运算符的简单示例:

int a = 5, b = 3;
int result;

result = a + b; // 加法
result = a > b ? a : b; // 条件运算符,选择a和b之间的较大者

int flag = 1;
flag = !flag; // 逻辑非,flag的值变为0

result = a & b; // 位与运算

a++; // 递增a的值

表达式是运算符和操作数组合成的序列。

示例:
int sum = 10 + 5; // '+' 是运算符, '10 + 5' 是表达式,10,5 是操作数

4.语句

C语言中的语句是构成程序的基本单位,用于表达特定的操作或逻辑。它们可以控制程序的流程、执行计算、调用函数等。语句以分号(;)结束,形成了程序的执行步骤。

C 语言的语句可以分为以下几类:

表达式语句:

最常见的语句,执行一个操作,如赋值、函数调用等,并以分号结束。

a = b + c;
printf("Hello, World!\n");
复合语句(块)

由花括号{}包围的一系列语句和声明,允许将多个语句视为单个语句序列。

{
    int x = 1;
    printf("%d\n", x);
}
条件语句

根据表达式的真假来执行不同的代码块。

  • if语句:是最基本的条件语句,根据条件的真假来执行相应的代码块。
if (condition) {
    // 条件为真时执行
} else {
    // 条件为假时执行
}
  • switch语句:根据表达式的值选择多个代码块之一执行。
switch(expression) {
    case constant1:
        // 表达式等于 constant1 时执行
        break;
    case constant2:
        // 表达式等于 constant2 时执行
        break;
    default:
        // 无匹配时执行
}
循环语句

重复执行一段代码直到给定的条件不满足。

  • while循环:先判断条件,条件满足则执行循环体。
while (condition) {
    // 条件为真时执行
}
  • do-while循环:先执行一次循环体,然后判断条件。
do {
    // 至少执行一次
} while (condition);
  • for循环:在循环开始时初始化变量,然后判断条件,最后在每次循环结束时执行更新表达式。
for (initialization; condition; increment) {
         // 条件为真时执行
}
// 示例:
for (int i = 0; i < 5; i++) {
    printf("%d ", i);
}
跳转语句

提供了改变代码执行顺序的能力。

  • break语句:用于立即退出最近的switch或循环(whiledo-whilefor)语句。
for (int i = 0; i < 10; i++) {
    if (i == 5) 
        break; // 当 i 等于5时退出循环 
}
  • continue语句:跳过当前循环的剩余部分,并继续下一次循环的执行(仅适用于whiledo-whilefor循环)。
for (int i = 0; i < 10; i++) {
    if (i == 5) continue; // 当i等于5时,跳过当前循环的剩余部分
}
  • goto语句:将控制转移到程序中标记的位置。尽管存在,但建议避免使用goto,因为它使得程序的流程变得难以追踪和理解。
label:
    printf("This is a label.");

goto label;     

数据类型

数据类型的一个常见用途就是:定义变量。常见的数据类型可以大致分为以下几个类别:

1.基本类型

整型

整数类型用于存储整数值,可以是有符号的(可以表示负数)或无符号的(仅表示非负数)。其中 int 是最常用的整数类型。为了适应不同的精度需求和内存大小限制,C语言提供了几种不同大小的整数类型。

有符号整型

  • short int 或简写为short,用于存储较小范围的整数。它至少占用16位(2个字节)的存储空间。
  • int 是最基本的整数类型,用于存储标准整数。在大多数现代编译器和平台上,它占用32位(4个字节)。
  • long int 或简写为 long,用于存储比int更大范围的整数。它至少占用32位,但在一些平台上可能会占用64位(8个字节)。
  • long long int 或简写为 long long,是C99标准引入的,用于提供更大范围的整数存储。它保证至少占用64位(8个字节)。

示例:

int a = 5;         // 定义一个整形变量,初始值为5
long b = 100000L;  // 定义一个长整形变量,初始值为100000

无符号整型

  • unsigned short int 或简写为 unsigned short,专门用于存储较小范围的正整数或零。这种类型至少占用16位(2个字节)的存储空间。

  • unsigned int 是用于存储标准大小的非负整数的基本类型。在大多数现代编译器和平台上,它占用32位(4个字节)。

  • unsigned long int 或简写为 unsigned long,用于存储大范围的非负整数。这种类型至少占用32位,在其他平台上可能占用64位(8个字节)

  • unsigned long long int 或简写为 unsigned long long,是为了在需要非常大范围的正整数时使用的。按照C99标准规定,它至少占用64位(8个字节)。

示例:

unsigned int x = 150;
unsigned long y = 100000UL;

浮点型:

浮点类型用于存储实数(小数点数字),包括 float 和 double。

示例:

float f = 5.25f;
double d = 10.75;

浮点型使用场景:float 和 double:用于需要表示小数的场景,如科学计算、金融计算等。float 提供了足够的精度,适合大多数应用,而 double 提供了更高的精度,适用于需要非常精确的计算结果的场景。

布尔类型:
布尔类型 bool 用于表示真(true)或假(false)。

示例:

bool flag = true;

布尔类型使用场景:bool 类型一般用于逻辑判断,表示条件是否满足。常用于控制语句(如if、while)的条件表达式,或表示函数返回的成功、失败状态。

枚举类型:
枚举(enum)允许定义一组命名的整数常量。使用关键字enum定义枚举类型变量。

示例:

enum day {sun, mon, tue, wed, thu, fri, sat};
enum day today = mon;

枚举类型使用场景
枚举类型 enum 用于定义一组命名的整数常量,使程序更易于阅读和维护。常用于表示状态、选项、配置等固定的集合。

2.复合类型:

结构体

结构体(struct)允许将多个不同类型的数据项组合成一个类型。使用关键字struct定义结构体。

示例:

// 定义 Person 结构体
struct Person {
    char name[50];
    int age;
};

// 定义结构体变量 person1
struct Person person1;

结构体类型使用场景:用于组合不同类型的数据项,表示具有结构的数据。

  • 表示实体或对象:用于封装和表示具有多个属性的复杂实体,如人、书籍、产品等。

  • 数据记录:组织和管理具有多个相关字段的数据记录,适用于数据库记录、日志条目等。

  • 网络编程:构造和解析网络协议的数据包,适用于客户端和服务器之间的通信。

  • 创建复杂的数据结构:作为链表、树、图等复杂数据结构的基本构建块,通过指针连接结构体实现。

联合体

联合体(union)在 C 语言中是一个用于优化内存使用的特殊数据类型,允许在同一内存位置存储不同的数据类型,但任一时刻只能使用其中一个成员。联合体变量使用关键字union 来定义。

示例:

union Data {
    int i;
    float f;
    char str[20];
};
union Data data;

联合体类型使用场景

  • 节省内存:当程序中的变量可能代表不同类型的数据,但不会同时使用时,联合体能有效减少内存占用。

  • 底层编程:在需要直接与硬件交互,或需要精确控制数据如何存储和解读时,联合体提供了直接访问内存表示的能力。

  • 网络通信:用于根据不同的协议或消息类型解析同一段网络数据。

  • 类型转换:允许以一种数据类型写入联合体,然后以另一种类型读取,实现不同类型之间的快速转换。

3.派生类型:

数组

数组是一种派生类型,它允许存储固定数量的同类型元素。当然,类型可以是多种,整形,浮点型,结构体等类型。在内存中,数组的元素按顺序紧密排列。数组的使用使得数据管理更加方便,尤其是当你需要处理大量同质数据时。

同质数据:具有相同数据类型的元素或值

示例:

// 定义
int arr[5] = {1, 2, 3, 4, 5};
printf("%d\n", numbers[0]); // 输出数组的第一个元素

数组的索引从0开始,numbers[0]表示数组中的第一个元素。

指针

指针是存储另一个变量地址的变量。指针在C语言中非常重要,它提供了直接访问内存的能力,使得程序可以通过地址来操作变量。

声明和使用指针

int var = 10;
int *ptr = &var; // 声明一个指针 ptr,并将其初始化为 var 的地址

通过指针访问值:

printf("Value of var: %d\n", *ptr); // 使用解引用操作符*来访问指针指向的值

数组

上面有提到过数组的概念,接下来让我们来详细讲解下数组:

数组是一种存储固定数量同类型元素的线性集合。在C语言中,这意味着如果你有一组相同类型的数据要存储,比如一周内每天的温度,那数组就是你的首选。

声明与初始化

声明数组的语法相当直观。比如,你想存储5个整数,可以这样声明:

int days[len];

这里,int表明了数组中元素的类型,days是数组的名称,而[len]则指定了数组可以存储元素的个数,len 必须是数值常量。

初始化数组可以在声明的同时进行,确保数组中的每个元素都有一个明确的起始值:

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

如果数组的大小在初始化时已知,你甚至可以省略大小声明:

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

访问与遍历

数组的元素可以通过索引(或下标)进行访问,索引从0开始,这意味着在上面的 days 数组中,第一个元素是days[0],最后一个元素是days[4]

遍历数组,即访问数组中的每个元素,通常使用循环结构,如for循环:

for(int i = 0; i < 5; i++) {
    printf("%d\n", days[i]);
}

多维数组

多维数组是一种直接在类型声明时定义多个维度的数组。它们通常用于存储具有多个维度的数据,如矩阵或数据表。

定义和初始化

多维数组的定义遵循这样的格式:类型 名称[维度1大小][维度2大小]…[维度N大小];

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

这定义了一个2x3的整型矩阵,并进行了初始化。

访问元素

访问多维数组的元素需要提供每一个维度的索引:数组名[索引1][索引2]…[索引N];

int value = matrix[1][2]; // 访问第二行第三列的元素

动态数组

动态数组提供了一种在运行时确定数组大小的能力,通过动态内存分配函数来实现。

动态一维数组:

动态一维数组通常通过指针和 malloc 或 calloc 函数创建:

malloc 分配的内存是未初始化的,而 calloc 会将内存初始化为零。

int *arr = (int*)malloc(10 * sizeof(int)); // 分配10个整数的空间
动态多维数组:

动态多维数组的创建稍微复杂,因为需要为每个维度分别进行内存分配:

int **matrix = (int**)malloc(2 * sizeof(int*)); // 创建2个指针的数组,对应2行
for(int i = 0; i < 2; i++) {
    matrix[i] = (int*)malloc(3 * sizeof(int)); // 为每行分配3个整数的空间
}

使用完动态数组后,必须手动释放其内存以避免内存泄漏

动态一维数组内存的释放:
free(arr)
动态多维数组内存的释放:
for(int i = 0; i < 2; i++) {
    free(matrix[i]); // 释放每一行
}
free(matrix); // 最后释放指针数组

数组与函数

在 C 语言中,数组可以作为参数传递给函数。不过,由于数组在传递时会退化为指向其首元素的指针,我们需要另外传递数组的大小:

int array[10] = {1,2,3,4,5,6,7,8,9,10};
int len = sizeof(array)/sizeof(array[0]); //计算数组的长度
printArray(array,len);
//printArray(array,len) 被调用时,printArray函数形参arr会退化成 int*

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

数组使用场景:

  • 固定大小集合:适用于存储已知数量的数据元素。
  • 顺序访问和高效索引:数组元素存储在连续的内存地址中,可以通过索引快速访问。
  • 多维数据表示:可以方便地表示多维数据结构,如二维数组表示矩阵。

指针

在 C 语言中,指针是一种特殊的变量类型,它的值是内存中另一个变量的地址。指针提供了一种方式来间接访问和操作内存中的数据。

可以把指针想象成一个指向内存中某个位置的箭头。每个变量都占用内存中的一定空间,指针的作用就是记录那个空间的起始地址。

定义指针

指针的定义需要指定指针类型,它表明了指针所指向的数据的类型。定义指针的一般形式是:

type* pointerName;

其中 type 是指针所指向的数据的类型,*表示这是一个指针变量,pointerName 是指针变量的名称。

示例:

int* ptr; // 定义一个指向int类型数据的指针

这个声明创建了一个名为ptr的指针,它可以指向int类型的数据。开始时,ptr未被初始化,它可能包含任意值(即任意地址)。在使用指针之前,通常会将其初始化为某个变量的地址,或者通过动态内存分配函数分配的内存块的地址。

指针的初始化

指针可以通过使用地址运算符 & 来获取变量的地址进行初始化:

int var = 10;
int* ptr = &var; // ptr现在指向var

或者,指针也可以被初始化为动态分配的内存地址:

int* ptr = (int*)malloc(sizeof(int)); // ptr指向一块新分配的int大小的内存

使用指针

解引用(Dereferencing)

通过解引用操作*,可以访问或修改指针所指向的内存位置中存储的数据。

*ptr = 20; // 修改ptr所指向的内存中的值为20
printf("%d ",*ptr); // 输出指针指向的数据
指针运算

指针的真正强大之处在于它能进行算术运算,这使得通过指针遍历数组和访问数据变得非常高效。

  • 递增(++):指针递增,其值增加了指向类型的大小(如int是4字节)。
  • 递减(–):与递增相反,指针递减会减去指向类型的大小。
  • 指针的加减:可以将指针与整数相加或相减,改变其指向。

指针递增(++)

int arr[] = {10, 20};
int *ptr = arr;
ptr++; // 现在指向arr[1]

指针递减(–)

ptr--; // 回到arr[0]

指针的加减

ptr += 1; // 移动到arr[1]
ptr -= 1; // 回到arr[0]

指针与数组

数组名在表达式中会被当作指向其首元素的指针。这意味着数组和指针在很多情况下可以互换使用。

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // 指向数组首元素的指针

指针与函数

函数参数为指针,通过传递指针给函数,可以让函数直接修改变量的值,而不是在副本上操作。

void addTen(int *p) {
    *p += 10; // 直接修改原始值
}

返回指针的函数

函数也可以返回指针,但要确保指针指向的是静态内存或者是动态分配的内存,避免悬挂指针。

// 返回静态内存地址
int* getStaticValue() {
    static int value = 10; // 静态局部变量
    return &value;
}

// 返回动态分配内存地址
int* getDynamicArray(int size) {
    return (int*)malloc(size * sizeof(int)); // 动态分配内存
}

避免悬挂指针

悬挂指针是指向已经释放或无效内存的指针。如果函数返回了指向局部非静态变量的指针,就会导致悬挂指针的问题,因为局部变量的内存在函数返回后不再有效。

int* getLocalValue() {
    int value = 10; // 局部变量
    return &value; // 错误:返回指向局部变量的指针
}

这是错误的做法,因为 value 是局部变量,在函数结束时被销毁,返回的指针指向一个已经不存在的内存位置。

函数指针

函数指针存储了函数的地址,可以用来动态调用函数。

// 函数原型
void greet(void) {
    printf("Hello, World!\n");
}

// 函数指针声明
void (*funcPtr)(void) = &greet;

// 通过函数指针调用函数
funcPtr();

指针数组与函数

指针数组可以用来存储函数指针,实现函数的动态调用。

// 定义两个简单的函数
void hello() {
    printf("Hello\n");
}
void world() {
    printf("World\n");
}
int main() {
    // 创建一个函数指针数组并初始化
    void (*funcPtrs[2])() = {hello, world};
    
    // 动态调用函数
    funcPtrs[0](); // 调用hello函数
    funcPtrs[1](); // 调用world函数
    return 0;
}

这个示例中,funcPtrs是一个存储函数指针的数组。通过指定索引,我们可以动态地调用hello或world函数。

多级指针

多级指针,例如二级指针,是指针的指针。它们在处理多维数组、动态分配的多维数据结构等场景中非常有用。

int var = 5;
int *ptr = &var;
int **pptr = &ptr; // 二级指针

指针使用场景:

  • 动态内存管理:配合malloc、realloc、calloc等函数,实现运行时的内存分配和释放。
  • 函数参数传递:允许函数通过指针参数修改调用者中的变量值。
  • 数组和字符串操作:通过指针算术运算灵活地遍历和操作数组和字符串。
  • 构建数据结构:是实现链表、树、图等复杂数据结构的基础。

字符串

在 C 语言中,字符串是以字符数组的形式存在的,以空字符 \0(ASCII码为0的字符)结尾。这意味着,当 C 语言处理字符串时,它会一直处理直到遇到这个空字符。理解这一点对于正确操作 C 语言中的字符串至关重要。

声明和初始化字符串

在 C 语言中,可以使用字符数组来声明和初始化字符串:

char greeting[6] = {'H', 'e', 'l', 'l', 'o', '\0'};

更简单的方式是使用字符串字面量,编译器会自动在字符串末尾加上\0:

char greeting[] = "Hello";

字符串除了使用字符数组来表示还可以用字符指针。

char *greeting = "Hello";

字符串的输入和输出

使用 printf 函数输出字符串,使用%s格式指定符:

printf("%s\n", greeting);

使用 scanf 函数读取字符串(注意,scanf在读取字符串时会因空格、制表符或换行符而停止读取):

int var;
scanf("%d", &var); // 输入整型值

char name[50];
scanf("%s", name); // 输入字符串
// 不需要&符号,因为数组名本身就是地址

字符串操作函数

C 标准库(string.h)提供了一系列操作字符串的函数,包括字符串连接、复制、长度计算等。

字符串复制
  • strcpy(destination, source):复制source字符串到destination。
  • strncpy(destination, source, n):复制最多n个字符从source到destination。
字符串连接
  • strcat(destination, source):将source字符串追加到destination字符串的末尾。
  • strncat(destination, source, n):将最多n个字符从source字符串追加到destination字符串的末尾。
字符串比较
  • strcmp(str1, str2):比较两个字符串。如果str1与str2相同,返回0
  • strncmp(str1, str2, n):比较两个字符串的前n个字符。
字符串长度
  • strlen(str):返回str的长度,不包括结尾的\0
字符串查找
  • strchr(str, c):查找字符c在str中首次出现的位置。
  • strrchr(str, c):查找字符c在str中最后一次出现的位置。
  • strstr(haystack, needle):查找字符串needle在haystack中首次出现的位置。
  • strspn(str1, str2):返回str1中包含的仅由str2中字符组成的最长子串的长度。
  • strcspn(str1, str2):返回str1中不含有str2中任何字符的最长子串的长度。
  • strpbrk(str1, str2):搜索str1中的任何字符是否在str2中出现,返回第一个匹配字符的位置。
其他
  • strdup(str):复制str字符串,使用malloc动态分配内存(非标准函数,但常见)。
  • memset(ptr, value, num):将ptr开始的前num个字节都用value填充。
  • memcpy(destination, source, num):从source复制num个字节到destination。
  • memmove(destination, source, num):与memcpy相似,但正确处理重叠的内存区域。

下面是一个简单的示例,展示如何使用部分字符串函数:

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

int main() {
    char str1[20] = "Hello";
    char str2[20] = "World";
    char str3[40];
    
    // 字符串连接
    strcpy(str3, str1);
    strcat(str3, " ");
    strcat(str3, str2);
    
    printf("%s\n", str3); // 输出 "Hello World"

    // 字符串比较
    if (strcmp(str1, str2) < 0)
        printf("\"%s\" is less than \"%s\"\n", str1, str2);
    
    // 字符串查找
    char *p = strstr(str3, "World");
    if (p) {
        printf("Found \"World\" in \"%s\"\n", str3);
    }
    return 0;
}

函数

函数是一种让程序结构化的重要方式,允许代码的重用、模块化设计和简化复杂问题。
C 语言支持自定义函数和标准库函数。

函数定义与声明:

定义指明了函数的实际代码体(即函数做什么和如何做)。

声明告诉编译器函数的名称、返回类型和参数(类型和数量),但不定义具体的操作。

示例:

/* 
函数声明
int :函数返回类型, add : 函数名
int x, int y :参数
*/
int add(int x, int y);

// 函数定义
int add(int x, int y) {
    return x + y;
}

函数参数传递

  • 按值传递:函数收到参数值的副本。在函数内部对参数的修改不会影响原始数据。
  • 按指针(地址)传递:通过传递参数的地址(使用指针),函数内的变化可以影响原始数据。
// 按值传递
void changeValue(int num) {
    num = 100; // 尝试修改,实际不会影响原始值
}

int main() {
    int x = 5;
    // x的副本被传递
    changeValue(x);  // 函数调用:执行函数
    // x的值不变,仍然是5
}

// 按指针(地址)传递
void changeReference(int *num) {
    *num = 100; // 通过指针修改原始值
}

int main() {
    int x = 5;
    change(x); // 传递x的地址
    // x的值现在变成了100
}

函数调用:

函数调用是 C 语言中一个核心概念,它允许在程序的任何地方执行一个函数。函数调用的基本过程包括将控制权从调用函数(caller)转移到被调用函数(callee),执行被调用函数的代码体,然后返回控制权给调用函数。

函数调用过程:

1. 参数传递:当调用函数时,会按照函数定义的参数列表,将实际参数的值(按值传递)或地址(按指针传递)传递给函数。

2. 执行函数体:一旦参数被传递,控制权转移到被调用函数,开始执行函数体内的代码。

3. 返回值:函数完成其任务后,可以通过 return 语句返回一个值给调用者。如果函数类型为 void,则不返回任何值。

4. 控制权返回:函数执行完毕后,控制权返回到调用函数的位置,程序继续执行下一条语句。

递归函数

递归函数是一种直接或间接调用自身的函数。它通常用于解决可以分解为相似子问题的问题。

示例:计算阶乘:n!

int factorial(int n) {
    if (n <= 1) 
        return 1;
    else return n * factorial(n - 1);
}

使用场景:递归在算法领域非常有用,尤其适合处理分治法、快速排序、二叉树遍历等问题。

内联函数

使用 inline 关键字声明的函数,在编译时会将函数体的代码直接插入每个调用点,而不是进行常规的函数调用。这可以减少函数调用的开销,但增加了编译后的代码大小。

示例:

inline int max(int x, int y) {
    return x > y ? x : y;
}

使用场景:对于那些体积小、调用频繁的函数,使用inline可以减少函数调用的开销,如简单的数学运算和逻辑判断函数。

回调函数

函数指针使得 C 语言支持回调函数,即将函数作为参数传递给另一个函数。

void greet() {
    printf("Hello, World!\n");
}

void repeat(void (*callbackFunc)(), int times) {
    for(int i = 0; i < times; i++) {
        callbackFunc(); // 调用回调函数
    }
}
int main() {
    repeat(greet, 3); // 将greet函数作为参数传递给repeat函数
    return 0;
}

使用场景
允许库或框架的用户提供自定义代码片段,根据需要在框架内部调用,以实现特定功能。例如,自定义排序函数中的比较操作。

函数的分类:

  • 用户定义函数:由程序员定义的函数,用于执行特定任务。

  • 标准库函数:C 语言标准库提供的函数,常见的标准库函数包括以下几类:

    1. 输入和输出(stdio.h):用于格式化输入和输出,如printf和scanf,以及文件操作的fgets和fputs。

    2. 字符串处理(string.h):提供字符串操作的基本函数,包括复制(strcpy)、连接(strcat)、长度计算(strlen)和比较(strcmp)。

    3. 数学计算(math.h):包括幂函数(pow)、平方根(sqrt)和三角函数(sin, cos, tan)等数学运算。

    4. 内存管理(stdlib.h):用于动态内存分配和释放,包括malloc、free和realloc等函数。

内存管理

在 C 语言中,内存管理是一个核心概念,涉及到动态内存分配、使用和释放。理解如何在C语言中管理内存对于编写高效、可靠的程序至关重要。

基本概念

C语言中的内存大致可以分为两大部分:静态/自动内存分配和动态内存分配

静态内存分配:

静态内存分配发生在程序编译时,它为全局变量和静态变量分配固定大小的内存。这些变量在程序启动时被创建,在程序结束时才被销毁。例如,全局变量、static 静态变量都属于这一类。它的生命周期贯穿整个程序执行过程。

自动内存分配:

自动内存分配是指在函数调用时为其局部变量分配栈上的内存。这些局部变量只在函数执行期间存在,函数返回时它们的内存自动被释放。自动内存分配的变量的生命周期仅限于它们所在的函数调用栈帧内。

简而言之,静态内存分配涉及到整个程序运行期间都存在的变量,而自动内存分配涉及到只在特定函数调用期间存在的局部变量。

动态内存分配:

C语言中的动态内存分配是一种在程序运行时(而不是在编译时)分配和释放内存的机制。这允许程序根据需要分配任意大小的内存块,使得内存使用更加灵活。C 提供了几个标准库函数来管理动态内存,主要包括malloc、calloc、realloc和free。

下面让我们来看下这几个内存分配函数如何使用?

malloc :
malloc函数用来分配一块指定大小的内存区域。分配的内存未初始化,可能包含任意数据。

/*
参数:size 是要分配的字节大小。
返回值:成功时返回指向分配的内存的指针;如果分配失败,则返回NULL。
注意:malloc 分配的内存内容是未初始化的,数据可能是未知的。
*/

void* malloc(size_t size); // 函数原型

#include <stdlib.h>
int *ptr = (int*)malloc(sizeof(int) * 5); // 分配一个5个整数大小的内存块
if (ptr != NULL) {
    // 使用ptr
    free(ptr); // 释放内存
}

释放内存

使用 free 函数来释放之前通过malloc、calloc或realloc分配的内存。释放后的内存不能再被访问,否则会导致未定义行为(如程序崩溃)。

释放内存后,已释放内存的指针称为悬挂指针。尝试访问已释放的内存将导致未定义行为。为了避免这种情况,释放内存后应将指针设置为 NULL。

free(ptr);
ptr = NULL;

内存泄漏

如果忘记释放已分配的动态内存,这部分内存将无法被再次利用,导致内存泄漏。长时间运行的程序如果频繁泄漏内存,可能会耗尽系统资源。

检测和避免

检测工具

  • Valgrind:Linux下一个广泛使用的内存调试工具,可以帮助开发者发现内存泄漏、使用未初始化的内存、访问已释放的内存等问题。
  • Visual Leak Detector (VLD):专为 Windows 平台开发的内存泄露检测工具,集成于Visual Studio,用于检测基于C/C++的应用程序。

避免策略
及时释放内存:确保每次malloc或calloc后,相应的内存不再需要时使用 free 释放。

预处理指令

C 语言中的预处理指令是在编译之前由预处理器执行的指令。它们不是 C 语言的一部分,而是在编译过程中的一个步骤,用于文本替换、条件编译等。预处理指令以井号(#)开头。

下面是一些常见的预处理指令及其使用示例:

#include 指令

#include指令用于包含一个源代码文件或标准库头文件。它告诉预处理器在实际编译之前,将指定的文件内容插入当前位置。

#include <stdio.h> // 包含标准输入输出头文件
#include "myheader.h" // 包含用户定义的头文件

#define 指令

#define用于定义宏。它告诉预处理器,将后续代码中所有的宏名称替换为定义的内容。

#define PI 3.14
#define SQUARE(x) ((x) * (x))

#undef 指令

#undef 用于取消宏的定义。

#define TABLE_SIZE 100
#undef TABLE_SIZE // 取消 TABLE_SIZE 的定义

#if, #else, #elif, #endif 指令

这些指令用于条件编译。只有当给定的条件为真时,编译器才会编译这部分代码。

#define DEBUG 1
#if DEBUG
    printf("Debug information\n");
#endif

#ifdef 和 #ifndef 指令

#ifdef检查一个宏是否被定义,#ifndef检查一个宏是否未被定义。

// 定义宏 DEBUG
#define DEBUG
#ifdef DEBUG
    printf("Debug mode is on.\n");
#endif

#ifndef DEBUG
    printf("Debug mode is off.\n");
#endif

#error 和 #pragma 指令

#error 指令允许程序员在代码中插入一个编译时错误。当预处理器遇到#error指令时,它会停止编译过程,并显示紧跟在#error后面的文本消息。这对于指出代码中的问题、配置错误或不支持的编译环境非常有用。

#if defined(_WIN64) || defined(__x86_64__)
    #define SUPPORTED_PLATFORM
#endif

#ifndef SUPPORTED_PLATFORM
    #error "This code must be compiled on a supported platform."
#endif

_WIN64和__x86_64__是预定义的宏,它们通常在编译器层面被定义,用于指示特定的平台或架构。
这些宏不是在用户的源代码中定义的,而是由编译器根据目标编译平台自动定义。

#pragma 指令用于提供编译器特定的指令,其行为依赖于使用的编译器。它通常用于控制编译器的特定行为,如禁用警告、优化设置或其他编译器特定的特性。

#pragma once  // 防止头文件内容被多次包含

#pragma GCC optimize ("O3") // 指示GCC编译器使用O3级别的优化

下面是一个简单的示例,展示了如何使用预处理指令来控制代码的编译。

#include <stdio.h>
#define DEBUG 1
int main() {
    #ifdef DEBUG
    printf("Debugging is enabled.\n");
    #endif
    printf("Program is running.\n");
    return 0;
}

在这个示例中,如果DEBUG被定义,则程序会打印调试信息。这是通过条件编译指令#ifdef实现的。

C 语言的预处理指令是编写灵活和高效代码的强大工具。通过合理使用预处理指令,可以实现条件编译、调试开关等功能,从而提升代码的可维护性和性能。

输入和输出

在C语言中,输入和输出(I/O)是基于数据流的概念。数据流可以是输入流或输出流,用于从源(如键盘、文件)读取数据或向目标(如屏幕、文件)写入数据。C标准库stdio.h提供了丰富的I/O处理函数。

基础概念

数据流

1. 输入流:数据从输入源(如键盘、文件)流向程序。

2. 输出流:数据从程序流向输出目标(如显示器、文件)。

3. 标准流

  • stdin:标准输入流,通常对应于键盘输入。
  • stdout:标准输出流,通常对应于屏幕输出。
  • stderr:标准错误流,用于输出错误消息,即使在标准输出被重定向的情况下,错误信息通常也会显示在屏幕上。

4. 文件流

除了标准的输入和输出流,C语言允许操作文件流,即直接从文件读取数据或向文件写入数据。

缓冲区

缓冲区是临时存储数据的内存区域,在数据在源和目标之间传输时使用。

分类

  • 全缓冲:当缓冲区满时,数据才会被实际地写入目的地。例如,向文件写入数据通常是全缓冲的。
  • 行缓冲:当遇到换行符时,数据才会被实际地写入目的地。例如,标准输出stdout(通常是终端或屏幕)就是行缓冲的。
  • 无缓冲:数据立即被写入目的地,不经过缓冲区。例如,标准错误 stderr 通常是无缓冲的

格式化输入、输出函数

scanf和printf:

printf 函数用于向标准输出(通常是屏幕)打印格式化的字符串。

scanf 函数用于从标准输入(通常是键盘)读取格式化的输入。

int age;
printf("Enter your age: ");
scanf("%d", &age);
printf("You are %d years old.\n", age);
getchar和putchar: 字符输入、输出

getchar 函数用于读取下一个可用的字符从标准输入,并返回它。

putchar 函数用于写一个字符到标准输出。

char ch;
printf("Enter a character: ");
ch = getchar();
putchar('You entered: ');
putchar(ch);
gets和puts : 字符串输入、输出

gets 函数用于从标准输入读取一行字符串直到遇到换行符。gets函数已经被废弃,因为它存在缓冲区溢出的风险,推荐使用fgets。

puts 函数用于输出一个字符串到标准输出,并在末尾自动添加换行符。

char str[100];
printf("Enter a string: ");
fgets(str, 100, stdin); // 使用fgets代替gets
puts("You entered: ");
puts(str);
文件操作

fopen、fclose、fgetc、fputc、fscanf、fprintf、feof、fseek、ftell、rewind

fopen 和 fclose 函数用于打开和关闭文件。

fgetc 和 fputc 用于读写单个字符到文件。

fscanf 和 fprintf 用于读写格式化的数据到文件。

feof 用于检测文件结束。

fseek 设置文件位置指针到指定位置。

ftell 返回当前文件位置。

rewind 重置文件位置指针到文件开始。

FILE *file = fopen("example.txt", "w");
if (file == NULL) {
    perror("Error opening file");
    return -1;
}
fprintf(file, "Hello, file!\n");
fclose(file);
错误处理: perror

perror 函数用于打印一个错误消息到标准错误。它将根据全局变量 errno 的值输出一个描述当前错误的字符串。

FILE *file = fopen("nonexistent.txt", "r");
if (file == NULL) {
    perror("Error");
    return -1;
}
fclose(file);

标准库

C语言的标准库提供了一系列广泛使用的函数,使得处理输入输出、字符串操作、内存管理、数学计算和时间操作等任务变得简单。下面是一些基本的标准库及其常用函数的简单介绍:

stdio.h

用途:输入和输出

常用函数:printf()(输出格式化文本),scanf()(输入格式化文本)

stdlib.h

用途:通用工具,如内存管理、程序控制

常用函数:malloc()(分配内存),free()(释放内存),atoi()(字符串转整数)

string.h

用途:字符串处理

常用函数:strcpy()(复制字符串),strlen()(计算字符串长度)

math.h

用途:数学计算

常用函数:pow()(幂运算),sqrt()(平方根)

time.h

用途:时间和日期处理

常用函数:time()(获取当前时间),localtime()(转换为本地时间)

其他

typedef

typedef 是一种关键字,用于为现有的数据类型创建一个新的名称(别名)。这在提高代码的可读性和简化复杂类型定义方面非常有用。

用途:
  • 定义复杂的数据结构:当你有一个复杂的结构体或联合体时,可以使用 typedef 给它一个更简单的名字。
  • 提高代码的可移植性:通过 typedef 定义平台相关的类型,使得代码更容易在不同平台间移植。
示例
typedef unsigned long ulong;

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

在这个例子中,ulong 现在可以用作 unsigned long 类型的别名,而 Point 可以用作上述结构体的类型名称。

命令行参数

命令行参数允许用户在程序启动时向程序传递信息。C 程序的 main 函数可以接受命令行参数,这是通过 main 函数的参数实现的:int argc, char *argv[]

用途
  • 参数个数(argc):表示传递给程序的命令行参数的数量。
  • 参数值(argv):是一个指针数组,每个元素指向一个参数的字符串表示。
示例:
#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("Program name: %s\n", argv[0]);
    if (argc > 1) {
        printf("Arguments: \n");
        for (int i = 1; i < argc; i++) {
            printf("%s\n", argv[i]);
        }
    } else {
        printf("No additional arguments were passed.\n");
    }
    return 0;
}

在这个示例中,程序首先打印出程序自己的名字(argv[0]),然后检查是否有其他命令行参数传递给程序,并打印它们。

总结

本篇文章主要是提供一个 C 语言入门的学习指南,帮助初学者快速入门 C 语言。

下面,让我们简短回顾下上文提到的知识点:

  • 基础语法:我们介绍了 C 语言的基本构建块,包括变量声明、数据类型和控制流结构,这是编写任何 C 程序的基础。
  • 数组和指针:这两个概念是 C 语言中管理数据集的核心工具。我们学习了如何通过它们高效地访问和操作数据。
  • 字符串处理:学习了 C 语言中字符串的操作和处理方法,包括字符串的基本操作如连接、比较和搜索。
  • 函数:介绍了函数的定义和使用,强调了封装和模块化代码的重要性,以提高程序的可读性和可维护性。
  • 内存管理:了解了C语言如何与计算机内存直接交互,包括动态分配、使用和释放内存的方法。
  • 预处理指令:讨论了预处理器如何在编译之前处理源代码,以及如何使用预处理指令来增强程序的可配置性和灵活性。
  • 输入和输出:我们学习了标准输入输出库的基本使用,理解了如何实现程序与用户之间的交互。
  • 标准库:介绍了C语言提供的强大标准库,它包括了一系列实用的函数和工具,用于处理字符串、数学计算、时间日期等。

最后:

本篇文章的目标就是让初学者知道如何学习 C 语言,哪些是重点?了解其基本的语法,能简单看懂代码即可。学完之后可以看这个通讯录项目,能看懂代码即可,如果可以优化一下就更好了。可以参考:Linux C/C++ 学习笔记(二):C语言实现通讯录 (该项目需要你稍微学习下数据结构中的单链表基本操作),如果你还不知道单链表是什么,可以看这篇文章:杨源鑫:一步一步教你从零开始写C语言链表(超详细)。当然你也可以自己百度或谷歌搜索C语言实现通讯录,找你能看懂的。

学习 C 语言只是学习编程的第一步,作为一门直接与硬件和操作系统打交道的计算机底层语言,要想掌握 C,你还得学习这几门课程:计算机组成原理、操作系统、数据结构。甚至,你还得学习汇编语言。除此之外,学会在 Linux 环境下进行 C 编程也是必须要掌握的。

如果你想学习 Linux 编程,包括系统编程和网络编程相关的内容,可以关注我的公众号「跟着小康学编程」,这里会定时更新计算机编程相关的技术文章,感兴趣的读者可以关注一下。具体可访问:关注小康微信公众号

另外大家在阅读这篇文章的时候,如果觉得有问题的或者有不理解的知识点,欢迎大家评论区询问,我看到就会回复。大家也可以加我的微信:jkfwdkf,备注「加群」,有任何不理解得都可以咨询我。

如果我的讲解对你有用的话,帮忙点个赞就再好不过啦!感谢!
  • 28
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值