C 语言程序设计基础

2023-11-14
C

数据类型

变量名只能由字母、数字和 _ 组成,且不能以字母开头。此外,变量名不可以为关键词。下面是 C 语言中常见的一些关键词。

autobreakcasecharconstcontinuedefaultdodoubleelseenumexternfloatforgotoifinlineintlongregisterrestrictreturnshortsignedsizeofstaticstructswitchtypedefunionunsignedvoidvolatilewhile

基础数据类型

类型大小范围
char1-128 到 127 或 0 到 255(取决于是否带符号)
unsigned char10 到 255
short2-32,768 到 32,767
unsigned short20 到 65,535
int4-2,147,483,648 到 2,147,483,647
unsigned int40 到 4,294,967,295
long4 或 8-2,147,483,648 到 2,147,483,647 或 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807(取决于系统)
unsigned long4 或 80 到 4,294,967,295 或 0 到 18,446,744,073,709,551,615(取决于系统)
long long8-9,223,372,036,854,775,808 到 9,223,372,036,854,775,807
unsigned long long80 到 18,446,744,073,709,551,615
float4大约 6~9 位精度,范围为 1.2e-38 到 3.4e38
double8大约 15 位精度,范围为 2.3e-308 到 1.7e308
long double16 或更多大约 19 位精度,范围为 3.4e-4932 到 1.1e4932(取决于系统)

ASCII 码表

序号字符序号字符序号字符序号字符
0NUL (空)32(空格)64@96`
1SOH (标题开始)33!65A97a
2STX (正文开始)3466B98b
3ETX (正文结束)35#67C99c
4EOT (传送结束)36$68D100d
5ENQ (询问)37%69E101e
6ACK (确认)38&70F102f
7BEL (响铃)3971G103g
8BS (退格)40(72H104h
9HT (横向制表)41)73I105i
10LF (换行)42*74J106j
11VT (纵向制表)43+75K107k
12FF (换页)44,76L108l
13CR (回车)45-77M109m
14SO (移出)46.78N110n
15SI (移入)47/79O111o
16DLE (退出数据链)48080P112p
17DC1 (设备控制1)49181Q113q
18DC2 (设备控制2)50282R114r
19DC3 (设备控制3)51383S115s
20DC4 (设备控制4)52484T116t
21NAK (反确认)53585U117u
22SYN (同步空闲)54686V118v
23ETB (传输块结束)55787W119w
24CAN (取消)56888X120x
25EM (媒介结束)57989Y121y
26SUB (替换)58:90Z122z
27ESC (退出)59;91[123{
28FS (文件分隔符)60<92\124|
29GS (组分隔符)61=93]125}
30RS (记录分隔符)62>94^126~
31US (单元分隔符)63?95_127DEL (删除)

参考:https://zh.cppreference.com/w/cpp/language/ascii

复合数据类型

结构体:一种能够存储不同类型数据的集合。

#include <stdio.h>

// 定义一个结构体
struct Person {
    char name[20];
    int age;
    float height;
};

int main() {
    // 声明一个结构体变量并初始化
    struct Person person1 = {"Alice", 25, 1.75};
    
    // 访问结构体成员并打印信息
    printf("Person's name: %s\n", person1.name);       // Person's name: Alice
    printf("Person's age: %d\n", person1.age);         // Person's age: 25
    printf("Person's height: %.2f\n", person1.height); // Person's height: 1.75
    
    return 0;
}

联合体:与结构体类似,但所有成员共享同一块内存空间。

#include <stdio.h>

// 定义一个联合
union Data {
    int i;
    float f;
    char str[20];
};

int main() {
    // 声明一个联合变量并使用
    union Data data;
    
    data.i = 10;
    printf("data.i: %d\n", data.i);   // data.i: 10
    
    data.f = 3.14; // 覆盖整型成员,为浮点型成员赋值
    printf("data.f: %.2f\n", data.f); // data.f: 3.14
    
    printf("data.i after changing data.f: %d\n", data.i);
    // data.i after changing data.f: 1078523331
    
    return 0;
}

表达式与运算符

C 语言中常见的几类运算符:

  • 算术运算符:+, -, *, /, %
  • 关系运算符:!=, ==, >, <, >=, <=
  • 逻辑运算符:!, &&, ||
  • 赋值运算符:=, +=, -=, …
  • 自增自减运算符:++, --
  • 条件运算符:?, :
  • 位运算符:&, |, ~, ^
  • 特殊运算符:&, ., ->

运算符优先级

运算符优先级决定了一个带有多个运算符的表达式如何进行计算。常见需要注意的是:

  • && 优先于 ||。例如要判断 a[30, 40] 之间,或者是 0 的话,可以直接写作 a == 0 || a > 30 && a < 40
  • [] 优先于 *。例如 *p[0] 指的是 p 的第 1 项解引用,也说明了 p 是一个指针数组(数组元素是指针),而 (*p)[] 则说明先解引用再取数组下标,代表 p 是一个数组指针(指针指向数组)。
优先级 运算符 描述 结合性
1 ++ -- 后缀自增与自减 从左到右
() 函数调用
[] 数组下标
. 结构体与联合体成员访问
-> 结构体与联合体成员通过指针访问
2 ++ -- 前缀自增与自减 从右到左
+ - 一元加与减
! ~ 逻辑非与逐位非
(type) 转型
* 间接(解引用)
& 取址
sizeof 取大小
3 * / % 乘法、除法及余数 从左到右
4 + - 加法及减法
5 << >> 逐位左移及右移
6 < <= 分别为 < 与 ≤ 的关系运算符
> >= 分别为 > 与 ≥ 的关系运算符
7 == != 分别为 = 与 ≠ 关系
8 & 逐位与
9 ^ 逐位异或(排除或)
10 | 逐位或(包含或)
11 && 逻辑与
12 || 逻辑或
13 ?: 三元条件 从右到左
14 = 简单赋值
+= -= 以和及差赋值
*= /= %= 以积、商及余数赋值
<<= >>= 以逐位左移及右移赋值
&= ^= |= 以逐位与、异或及或赋值
15 , 逗号 从左到右

参考:https://zh.cppreference.com/w/c/language/operator_precedence

控制结构

顺序结构

按照语句的顺序执行,没有特定的控制流程。

int a = 5;
int b = 10;
int sum = a + b; // 顺序执行,计算a和b的和

选择结构

if-else 语句:根据条件选择执行不同的代码块。

int x = 15;
if (x > 10) {
  printf("x大于10\n");
} else {
  printf("x不大于10\n");
}

switch 语句:根据不同的情况选择执行不同的代码块。

int day = 3;
switch (day) {
  case 1:
    printf("星期一\n");
    break;
  case 2:
    printf("星期二\n");
    break;
  default:
    printf("其他天\n");
}

循环结构

while 循环:在条件为真时重复执行代码块。

// while循环
int i = 0;
while (i < 5) {
  printf("%d\n", i);
  i++;
}

do-while 循环:先执行一次代码块,然后在条件为真时重复执行。

int j = 0;
do {
  printf("%d\n", j);
  j++;
} while (j < 5);

for 循环:按照给定的初始化、条件和递增/递减来重复执行代码块。

for (int k = 0; k < 5; k++) {
  printf("%d\n", k);
}

跳转结构

break 语句:在循环或switch语句中,用于跳出当前循环或switch。

for (int m = 0; m < 10; m++) {
  if (m == 5) {
    break; // 当m等于5时跳出循环
  }
  printf("%d\n", m);
}

continue 语句:在循环中,用于跳过剩余的循环体并开始下一次迭代。

for (int n = 0; n < 5; n++) {
  if (n == 2) {
    continue; // 当n等于2时跳过当前迭代
  }
  printf("%d\n", n);
}

goto 语句:通过标签跳转到代码的不同位置。

int number = 0;
loop: // 标签
if (number < 5) {
  printf("%d\n", number);
  number++;
  goto loop; // 跳转到标签loop处
}

作用域与生命周期

作用域

  • 函数作用域:在函数内部声明的变量具有函数作用域,只能在声明它们的函数内部使用。这些变量在函数执行期间存在,并在函数执行结束后被销毁。
  • 文件作用域:在函数外部声明的变量具有文件作用域,它们在整个文件内可见,但不能被其他文件访问。这些变量在程序执行期间存在,直到程序结束。
  • 块作用域:在代码块(由大括号 {} 包围的部分)内声明的变量具有块作用域,只能在该块内部使用。

生命周期修饰符

  • auto:默认的存储类别修饰符,用于声明具有函数作用域的变量。变量在函数内部自动分配内存,其生命周期与函数调用周期相同。
  • static:静态存储类别修饰符。在全局变量中,static 修饰符使变量具有文件作用域,只在声明它的文件内可见。在局部变量中,static 使变量具有函数作用域,并且其生命周期会延长到程序的整个执行过程。静态局部变量的值在函数调用之间保持不变,不会像普通局部变量一样在函数调用结束后销毁。它们的值在第一次函数调用时初始化,并在后续调用中保持之前的值。
  • extern:用于声明变量或函数的外部链接性,在一个文件中声明变量或函数,然后在其他文件中使用它们。extern 关键字告诉编译器该标识符在其他地方定义,而不是在当前文件中定义。声明具有 extern 关键字的变量或函数时,不分配存储空间,只是引用其他地方定义的标识符。

数组与指针

如果初始化数组但不赋值,那么数组的内容是随机的。如果使用 {} 初始化数组,那么没有显示声明的元素都会默认初始化为 0。

int fa[] = {};
int a[10];

// int a[];
// error: 不允许使用不完整的类型

int b[2] = {};

// int b[2] = {1, 2, 3};
// error: 初始值设定项太多

对于多维数组,仅允许空缺最高维。

int c[][5] = {{1, 2}, {1}};

// int c[][2] = {{1, 2, 3}};
// error: 初始值设定项太多

// int c[5][];
// error: 数组不能包含此类型的元素

数组名实际上是一个带有长度参数的指向数组首元素的静态指针。指针 + 1 实际上是 指针指向的地址 + sizeof(指针类型)

  int f[] = {10, 20};
  printf("%x %d %d\n", f, f[0], *f);
  // 52bffb08 10 10
  printf("%x %d %d\n", f + 1, f[1], *(f + 1));
  // 52bffb0c 20 20
  printf("%x %d %d\n", (void *)f + 1, f[1], *(int *)((void *)f + 1));
  // 52bffb09 20 335544320

指针传参数组时,只有最外层的数组长度可以不同(实际上是因为最外层的数组退化成了指针,丢失了长度信息)。

void fun(int p[4][2])
{
  for (int i = 0; i < 4; i++)
  {
  for (int j = 0; j < 2; j++)
    printf("%d ", p[i][j]);
  printf("\n");
  }
}

int a[3][2] = {1, 2, 3, 4, 5, 6};
int b[3][1], c[3][3];
fun(a);
// 1 2 
// 3 4
// 5 6
// -1475524368 371
// 超出的内容会直接溢出

// fun(b);
// cannot convert 'int (*)[1]' to 'int (*)[2]'

// fun(c);
// cannot convert 'int (*)[3]' to 'int (*)[2]'

基础排序算法

冒泡排序:一次比较两个元素,如果它们的顺序错误就将它们交换。

void bubbleSort(int arr[], int n) {
  for (int i = 0; i < n - 1; i++) {
    for (int j = 0; j < n - i - 1; j++) {
      if (arr[j] > arr[j + 1]) {
        int temp = arr[j];
        arr[j] = arr[j + 1];
        arr[j + 1] = temp;
      }
    }
  }
}

选择排序:每次从未排序的部分选择最小的元素,放到已排序部分的末尾。

void selectionSort(int arr[], int n) {
  int minIndex;
  for (int i = 0; i < n - 1; i++) {
    minIndex = i;
    for (int j = i + 1; j < n; j++) {
      if (arr[j] < arr[minIndex]) {
        minIndex = j;
      }
    }
    int temp = arr[i];
    arr[i] = arr[minIndex];
    arr[minIndex] = temp;
  }
}

插入排序:将未排序的元素插入到已排序部分的合适位置。

void insertionSort(int arr[], int n) {
  int key, j;
  for (int i = 1; i < n; i++) {
    key = arr[i];
    j = i - 1;
    while (j >= 0 && arr[j] > key) {
      arr[j + 1] = arr[j];
      j = j - 1;
    }
    arr[j + 1] = key;
  }
}

字符串

声明字符串的两种方式:

  • char \*str:字符串常量。存储在只读内存段,不可修改。
  • char str[]:字符数组。可以修改。

常用字符串库函数

  • strlen():计算字符串的长度,不包括字符串末尾的空字符 \0

    char str[] = "Hello";
    size_t length = strlen(str);
    printf("Length of string: %zu\n", length); // Output: Length of string: 5
  • strcpy():将一个字符串拷贝到另一个字符串中。

    char source[] = "Hello";
    char destination[20];
    strcpy(destination, source);
    printf("Destination: %s\n", destination); // Output: Destination: Hello
  • strcat():将一个字符串追加到另一个字符串的末尾。

    char str1[20] = "Hello";
    char str2[] = " World";
    strcat(str1, str2);
    printf("Concatenated string: %s\n", str1); // Output: Concatenated string: Hello World
  • strcmp():比较两个字符串的大小关系。

    char str1[] = "apple";
    char str2[] = "banana";
    int result = strcmp(str1, str2);
    if (result < 0) {
      printf("str1 is less than str2\n");
    } else if (result > 0) {
      printf("str1 is greater than str2\n");
    } else {
      printf("str1 is equal to str2\n");
    }
  • strstr():在一个字符串中查找另一个字符串第一次出现的位置。

    char str[] = "Hello, how are you?";
    char substr[] = "how";
    char* found = strstr(str, substr);
    if (found != NULL) {
      printf("Substring found at position: %ld\n", found - str); // Output: Substring found at position: 7
    } else {
      printf("Substring not found\n");
    }

其他

输入与输出

  • printf(const char *format, ...):将格式化数据输出到控制台。
  • scanf(const char *format, ...):从控制台读取输入数据。
  • puts(const char *str):输出字符串到控制台,并自动添加换行符。
  • gets(char *str):从标准输入中获取一行字符串。(存在安全隐患,不推荐使用
  • putchar(int character):将一个字符输出到控制台。
  • getchar(void):从标准输入中获取一个字符。

格式字符串

格式符号含义
%d以十进制形式输出整数
%ld以长整型(long)十进制形式输出整数
%u以无符号十进制形式输出整数
%lu以无符号长整型(unsigned long)十进制形式输出整数
%x以十六进制形式输出整数(小写字母)
%X以十六进制形式输出整数(大写字母)
%o以八进制形式输出整数
%c输出字符
%s输出字符串
%f以十进制形式输出浮点数
%e以指数形式输出浮点数(小写字母e)
%E以指数形式输出浮点数(大写字母E)
%g根据值的大小决定是用%f还是%e
%G根据值的大小决定是用%f还是%E
%p输出指针的地址
%%输出百分号 %
%5d输出宽度为5的十进制整数,右对齐
%-5d输出宽度为5的十进制整数,左对齐
%05d输出宽度为5的十进制整数,用0填充空白
%8.2f输出宽度为8的浮点数,保留2位小数
%-8.2f输出宽度为8的浮点数,保留2位小数,左对齐
%10s输出宽度为10的字符串,右对齐
%-10s输出宽度为10的字符串,左对齐

随机数

在 C 语言中,可以使用 rand() 函数来生成伪随机数。通常情况下,随机数的范围在 0 到 32767 之间。

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

int main() {
  int i, n;
  // 设置随机数种子
  srand(time(0));

  n = rand();      // 生成随机数
  n = rand() % 10; // 生成0到10之间的随机数
  
  return 0;
}

链表

链表是一种常见的线性数据结构,它由一系列节点组成,每个节点包含两部分:数据部分和指向下一个节点的指针。链表中的节点按照线性顺序排列,但在内存中可以不是连续存储的。

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

struct Node {
  int data;
  struct Node* next;
};

void append(struct Node* head , int data) {
  struct Node* p = head;
  struct Node* new_node = (struct Node*)malloc(sizeof(struct Node));
  new_node->data = data;
  new_node->next = NULL;

  while (p->next != NULL) {
    p = p->next;
  }
  p->next = new_node;
}