C语言基础语法

数据类型与变量

(1)变量是内存中的一个存储区域,该区域的数据可以在同一类型范围内不断变化。 (2)通过变量名,可以引用这块内存区域,获取里面存储的值。 (3)变量的构成包含三个要素:数据类型、变量名、存储的值。

变量必须先声明,后使用。可以先声明变量再赋值,也可以在声明变量的同时进行赋值。

void,表示"无类型", 主要用途:

  • 函数不返回任何值:void func()
  • 函数无参数:int func(void)
  • 通用指针:void*

整数类型

(1) 基本整数类型

类型 存储大小 (通常) 取值范围 (通常) 格式化符号
char 1字节 -128 到 127 或 0 到 255 %c
short 2字节 -32,768 到 32,767 %hd
int 4字节 -2,147,483,648 到 2,147,483,647 %d
long 4或8字节 取决于平台 %ld
long long 8字节 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 %lld

(2) 修饰符

  • signed:有符号数(默认)
  • unsigned:无符号数(范围从0开始)
  • short:缩短整数长度
  • long:扩展整数长度
c
unsigned char byte = 255;  // 0 到 255
signed short temperature = -30;
unsigned long population = 7800000000;

C99新增类型: 固定宽度整数类型

c
#include <stdint.h>

int8_t small;     // 精确8位有符号整数
uint16_t medium;  // 精确16位无符号整数
int32_t large;    // 精确32位有符号整数
int64_t huge;     // 精确64位有符号整数

浮点类型

类型 存储大小 精度 取值范围 格式化符号
float 4字节 6-7位小数 ±1.2×10^-38 到 ±3.4×10^38 %f
double 8字节 15-16位小数 ±2.3×10^-308 到 ±1.7×10^308 %lf
long double 10或16字节 19-20位小数 更大范围 %Lf
c
float pi = 3.14159f;
double atomic_mass = 1.66053906660e-27;
long double very_precise = 3.141592653589793238L;

C99新增类型: 复数类型

c
#include <complex.h>

double complex z = 1.0 + 2.0 * I;

数据类型转换

1. 隐式类型转换

自动发生的转换,遵循类型提升规则

c
int i = 10;
float f = 3.14;
double d = i + f;  // i转换为float,然后结果转换为double

2. 显式类型转换(强制转换)

c
double x = 3.14159;
int y = (int)x;  // y = 3

int a = 10, b = 3;
float result = (float)a / b;  // 3.333...

强制转换可能导致数据丢失,应谨慎使用。


布尔类型

C99标准引入的布尔类型(_Boolbool)本质上仍然是一种整数类型,其核心特点是:

  • 存储形式:实际仍以整数值存储(通常1字节)
  • 取值规范0表示假(false),任何非0值表示真(true)
  • 类型安全:相比传统方式,提供了更明确的语义标记

布尔类型的底层实现:

c
// C标准中的定义(通常实现方式)
#define bool _Bool
#define true 1
#define false 0

布尔变量在内存中仍占用至少1字节空间,但逻辑上只应包含0或1。

布尔类型标准头文件及基本用法:

c
#include <stdbool.h>

该头文件提供:

  • bool:布尔类型别名(实际为_Bool
  • true:值为1的常量
  • false:值为0的常量
c
bool is_raining = true;
bool is_sunny = false;

if (is_raining) {
    printf("Take an umbrella\n");
}

内存占用:

c
printf("%zu\n", sizeof(bool));  // 通常是1(但标准只要求至少1)

与旧代码的兼容

c
// 新旧布尔类型混用时的安全写法
#ifdef __STDC_VERSION__
#include <stdbool.h>
#else
typedef enum { false, true } bool;
#endif

常量的定义

在C语言中,常量是程序运行期间不可改变的值。C语言提供了两种主要方式来定义常量:使用预处理指令#define和使用const关键字。

常量是程序中固定不变的值,与变量相对。使用常量的好处包括:

  • 提高代码可读性(如PI3.14159更易理解)
  • 便于统一修改(只需修改一处定义)
  • 避免意外修改导致错误

#define 宏定义常量

  • 预处理阶段替换:在编译前由预处理器进行文本替换
  • 无类型检查:只是简单的文本替换,不进行类型验证
  • 不分配内存:因为只是文本替换,没有存储概念
  • 作用域:从定义处到文件末尾,或直到#undef
  • 常见用途:定义数值常量、字符串常量、条件编译
c
#include <stdio.h>

#define PI 3.14159
#define MAX_SIZE 100
#define WELCOME_MSG "Hello, World!"

int main() {
    double area = PI * 5 * 5;  // 编译前会被替换为 3.14159 * 5 * 5
    printf("%s\n", WELCOME_MSG);
    printf("Max elements: %d\n", MAX_SIZE);
    return 0;
}

const 常量

  • 编译期处理:由编译器处理,是真正的语言特性
  • 类型安全:有明确的类型,编译器会进行类型检查
  • 分配内存:通常分配只读存储空间(取决于实现)
  • 作用域:遵循C语言的作用域规则(块作用域、文件作用域等)
  • 常见用途:需要类型检查的常量、数组大小、函数参数
c
#include <stdio.h>

const double PI = 3.14159;
const int MAX_SIZE = 100;
const char WELCOME_MSG[] = "Hello, World!";

int main() {
    const int local_const = 42;  // 局部常量
    
    double area = PI * 5 * 5;    // 使用方式与变量相同
    printf("%s\n", WELCOME_MSG);
    printf("Max elements: %d\n", MAX_SIZE);
    printf("Local const: %d\n", local_const);
    
    return 0;
}

#defineconst 的关键区别

特性 #define const
处理阶段 预处理阶段(文本替换) 编译阶段
类型检查 无类型 有类型,编译器会检查
内存分配 不分配内存 通常分配只读内存
作用域 文件作用域(可被#undef取消) 遵循C语言作用域规则
调试可见性 调试器中不可见(已被替换) 调试器中可见
数组大小定义 可用于静态数组大小 C89中不能用于静态数组大小
指针使用 不能定义指向常量的指针 可以定义指向常量的指针
复合类型 不能定义结构体等复合类型常量 可以定义复合类型常量


注意事项

  1. 宏的副作用

    c
    #define SQUARE(x) x * x
    int result = SQUARE(2+3);  // 展开为 2+3 * 2+3 = 11,不是25
    // 应改为:
    #define SQUARE(x) ((x) * (x))
  2. C与C++的区别: 在C++中,const常量默认有内部链接(可通过extern改变), C++中const可以用于数组大小定义

  3. 存储位置const变量通常存储在只读数据段(取决于实现),某些嵌入式系统中可能存储在Flash而非RAM


现代C的最佳实践——对于C99及以上版本的项目:

c
// 现代C推荐方式
const double PI = 3.141592653589793;
const int MAX_USERS = 1000;

// 必须使用宏的情况
#define DEBUG_MODE 1
#ifdef DEBUG_MODE
    // 调试专用代码
#endif
  • 优先使用const获得类型安全,仅在不适合使用const时才用#define
  • 对宏定义使用全大写命名以便识别,为宏参数添加括号避免优先级问题

常见运算符及应用

C语言中的运算符是用于执行各种操作(如算术运算、逻辑判断、位操作等)的符号。它们在程序中广泛使用,用于处理数据、控制流程和实现复杂逻辑。


算术运算符

算术运算符用于执行基本的数学运算。

运算符 描述 示例 结果(假设 a = 10, b = 3
+ 加法 a + b 13
- 减法 a - b 7
* 乘法 a * b 30
/ 除法 a / b 3 (整数除法,取商)
% 取模(求余数) a % b 1 (余数为1)
++ 自增(前/后自增) a++++a a = 11
-- 自减(前/后自减) b----b b = 2

注意

  • /% 的行为取决于操作数类型:
    • 如果操作数是整数,则 / 执行整数除法。
    • 如果操作数是浮点数,则 / 执行浮点除法。
  • 自增和自减运算符:
    • 前置:先修改变量值,再使用。
    • 后置:先使用变量值,再修改。

关系运算符

关系运算符用于比较两个值,并返回布尔值(真或假)。C语言中用非零表示真,零表示假。

运算符 描述 示例 结果(假设 a = 10, b = 3
== 等于 a == b 0 (假)
!= 不等于 a != b 1 (真)
> 大于 a > b 1 (真)
< 小于 a < b 0 (假)
>= 大于等于 a >= b 1 (真)
<= 小于等于 a <= b 0 (假)

逻辑运算符

逻辑运算符用于组合多个条件表达式。

运算符 描述 示例 结果(假设 a = 1, b = 0
&& 逻辑与 a && b 0 (假)
|| 逻辑或 a || b 1 (真)
! 逻辑非 !a 0 (假)

注意

  • &&|| 具有短路特性:
    • 对于 &&,如果第一个条件为假,则不计算第二个条件。
    • 对于 ||,如果第一个条件为真,则不计算第二个条件。

位运算符

位运算符直接对整数的二进制位进行操作。

运算符 描述 示例 结果(假设 a = 5 (0101), b = 3 (0011)
& 按位与 a & b 1 (0001)
| 按位或 a | b 7 (0111)
^ 按位异或 a ^ b 6 (0110)
~ 按位取反 ~a -6 (补码表示)
<< 左移 a << 1 10 (1010)
>> 右移 a >> 1 2 (0010)

注意

  • 左移相当于将数值乘以2的幂次。
  • 右移相当于将数值除以2的幂次(对于无符号数)。

赋值运算符

赋值运算符用于将一个值赋给变量。

运算符 描述 示例 等价形式
= 简单赋值 a = b a = b
+= 加法赋值 a += b a = a + b
-= 减法赋值 a -= b a = a - b
*= 乘法赋值 a *= b a = a * b
/= 除法赋值 a /= b a = a / b
%= 取模赋值 a %= b a = a % b
<<= 左移赋值 a <<= b a = a << b
>>= 右移赋值 a >>= b a = a >> b
&= 按位与赋值 a &= b a = a & b
|= 按位或赋值 a |= b a = a | b
^= 按位异或赋值 a ^= b a = a ^ b

条件运算符

条件运算符是唯一的三目运算符,用于根据条件选择不同的值。

语法:condition ? value_if_true : value_if_false

c
int a = 10, b = 20;
int max = (a > b) ? a : b; // 如果 a > b,max = a;否则 max = b
printf("Max: %d\n", max); // 输出:Max: 20

其他运算符

  1. 逗号运算符 (,)

    • 用于将多个表达式连接在一起,整个表达式的值是最后一个表达式的值。
    • 示例:
      c
      int a = (1, 2, 3); // a = 3
      
  2. 取地址运算符 (&)指针解引用运算符 (*)

    • 用于操作内存地址。
    • 示例:
      c
      int x = 10;
      int *p = &x; // p 存储 x 的地址
      printf("%d\n", *p); // 输出:10
      
  3. sizeof 运算符

    • 返回变量或数据类型的大小(以字节为单位)。
    • 示例:
      c
      int size = sizeof(int); // size = 4(通常为4字节)
      
  4. 成员访问运算符

    • .:用于访问结构体或联合体的成员。
    • ->:用于通过指针访问结构体或联合体的成员。
    • 示例:
      c
      struct Point {
          int x, y;
      };
      struct Point p = {10, 20};
      printf("%d\n", p.x); // 输出:10
      
      struct Point *ptr = &p;
      printf("%d\n", ptr->y); // 输出:20
      

运算符优先级

C语言中的运算符具有不同的优先级和结合性,这决定了表达式的计算顺序。

优先级 运算符 结合性
1 () [] -> . 从左到右
2 ! ~ ++ -- 从右到左
3 * / % 从左到右
4 + - 从左到右
5 << >> 从左到右
6 < <= > >= 从左到右
7 == != 从左到右
8 & 从左到右
9 ^ 从左到右
10 | 从左到右
11 && 从左到右
12 `
13 ?: 从右到左
14 = += -= 从右到左
15 , 从左到右

建议: 在复杂的表达式中,使用括号明确优先级,以提高代码可读性和避免错误。


程序控制语句

C语言中的程序控制语句用于控制程序的执行流程,包括条件判断、循环和跳转等。这些控制语句使得程序可以根据不同的条件执行不同的代码块,或者重复执行某些操作,从而实现复杂的功能。

  1. 条件语句ifif-elseif-else if-elseswitch
  2. 循环语句forwhiledo-while
  3. 跳转语句breakcontinuegotoreturn

合理使用这些控制语句,可以使你的代码更加清晰、高效和易于维护。同时,建议避免过度使用 goto,以保持代码的可读性和结构化风格。


条件语句

  1. if 语句: 用于在满足某个条件时执行特定代码块。
c
int x = 10;
if (x > 5) {
    printf("x is greater than 5\n");
}
  1. if-else 语句: 用于在条件为真时执行一个代码块,否则执行另一个代码块。
c
int x = 3;
if (x > 5) {
    printf("x is greater than 5\n");
} else {
    printf("x is less than or equal to 5\n");
}
  1. if-else if-else 语句: 当需要检查多个条件时,可以使用 if-else if-else 结构。
c
int score = 85;
if (score >= 90) {
    printf("Grade: A\n");
} else if (score >= 75) {
    printf("Grade: B\n");
} else {
    printf("Grade: C\n");
}
  1. switch 语句:用于基于变量的值执行多个代码块之一。
  • 每个 case 后面必须有 break,否则会继续执行后续的 case 代码(称为“贯穿”)。
  • default 是可选的,用于处理没有匹配的 case 的情况。
c
int day = 3;
switch (day) {
    case 1:
        printf("Monday\n");
        break;
    case 2:
        printf("Tuesday\n");
        break;
    case 3:
        printf("Wednesday\n");
        break;
    default:
        printf("Invalid day\n");
}

循环语句

  1. for 循环: 用于重复执行一段代码固定次数或基于某个条件。
c
for (int i = 1; i <= 5; i++) {
    printf("%d ", i);
}
// 输出:1 2 3 4 5
  1. while 循环: 在条件为真时重复执行代码块。
c
int i = 1;
while (i <= 5) {
    printf("%d ", i);
    i++;
}
// 输出:1 2 3 4 5
  1. do-while 循环:至少执行一次循环体,然后在条件为真时继续执行。
c
int i = 1;
do {
    printf("%d ", i);
    i++;
} while (i <= 5);
// 输出:1 2 3 4 5

跳转语句

  1. break:立即退出当前循环或 switch 语句。
c
for (int i = 1; i <= 10; i++) {
    if (i == 5) {
        break; // 退出循环
    }
    printf("%d ", i);
}
// 输出:1 2 3 4
  1. continue:用于跳过当前迭代的剩余部分,直接进入下一次迭代。
c
for (int i = 1; i <= 5; i++) {
    if (i == 3) {
        continue; // 跳过 i == 3 的迭代
    }
    printf("%d ", i);
}
// 输出:1 2 4 5
  1. goto:用于无条件跳转到指定标签处。虽然不推荐频繁使用,但在某些情况下(如错误处理)可能有用。
c
int x = 10;
if (x > 5) {
    goto end;
}
printf("This will not be printed.\n");
end:
    printf("Jumped to the end.\n");
// 输出:Jumped to the end.
  1. return: 用于从函数中返回值,并结束函数的执行。
c
int add(int a, int b) {
    return a + b;
}

C语言标准输入输出

C语言的标准输入输出功能由stdio.h头文件提供的一系列函数支持。这些函数提供了与用户进行交互的能力,允许程序读取用户的输入或将信息输出给用户


printf() 函数

printf() 函数 用于格式化输出数据到终端或指定的输出流, int printf(const char *format, ...);

c
#include <stdio.h>

int main() {
    int num = 5;
    printf("The number is %d\n", num);
    return 0;
}

在这个例子中,%d是一个占位符,表示将要插入一个整数类型的值。

说明符 用途 示例
%d 十进制整数 123
%u 无符号十进制 456u
%f 浮点数 3.14159
%c 单个字符 ‘A’
%s 字符串 “text”
%p 指针地址 &variable
%x 十六进制(小写) 0x1a3
%X 十六进制(大写) 0X1A3
%% 百分号本身 %

printf()函数向标准输出(stdout)写入数据,默认情况下就是终端屏幕。它本质上是对标准输出流的操作。在底层,printf() 使用了 stdout 这个预定义的 FILE* 流指针。


scanf() 函数

scanf() 函数用于从标准输入(通常是键盘)读取数据并根据指定的格式存储到变量中: int scanf(const char *format, ...);

c
#include <stdio.h>

int main() {
    int age;
    printf("Enter your age: ");
    scanf("%d", &age);
    printf("Your age is %d\n", age);
    return 0;
}

注意这里需要使用变量的地址(&age),因为scanf()需要知道在哪里存储输入的数据。

格式化字符串的安全性:当使用printf()scanf()时,确保提供的格式字符串与实际参数匹配,否则可能导致未定义行为。

scanf()函数从标准输入(stdin)读取数据,默认情况下是从键盘读取。同样地,它也是对标准输入流的操作,使用了 stdin 这个预定义的 FILE* 流指针。


行 I/O 函数

  1. gets(char *str)puts(const char *str)
    • gets(char *str): 从标准输入读取一行文本并存储到指定的缓冲区str中,直到遇到换行符为止。不推荐使用
      • 安全问题:没有提供缓冲区大小限制,容易导致缓冲区溢出攻击,非常不安全。已经被C11标准弃用。
      • 替代方案:使用fgets(str, size, stdin)代替gets(str)
    • puts(const char *str): 向标准输出打印一个字符串,并自动追加一个换行符。不如fputs()灵活,因为它总是添加换行符且只能输出到标准输出。
      c
      puts("Hello, World!");

  1. fgets(char *str, int n, FILE *stream)fputs(const char *str, FILE *stream)
    • fgets:从指定流中读取最多n-1个字符(或直到遇到换行符\n)到缓冲区str,并在末尾添加一个空字符\0。相比gets(),它允许指定缓冲区大小,从而避免了缓冲区溢出的风险。
      c
      char buffer[100];
      if (fgets(buffer, sizeof(buffer), stdin) != NULL) {
          printf("You entered: %s", buffer);
      }
    • fputs:将字符串写入指定流中。与puts()不同的是,它不会自动添加换行符。适用于需要精确控制输出的情况。
    c
    fputs("Hello, World!\n", stdout);

  1. getline()getdelim()(POSIX标准) 这些函数不是ANSI C标准的一部分,但被广泛支持,特别是在Unix/Linux系统中。
    • ssize_t getline(char **lineptr, size_t *n, FILE *stream);: 动态分配内存来读取整行输入,适合处理未知长度的输入行。
    • ssize_t getdelim(char **lineptr, size_t *n, int delimiter, FILE *stream);: 类似于getline(),但是可以指定分隔符而不是默认的换行符。


字符 I/O 函数

字符I/O函数在C语言中用于处理单个字符的读写操作,它们提供了比格式化输入输出(如printf()scanf())更细粒度的控制。这些函数特别适用于需要逐字符处理文本的应用场景。

  1. fgetc(FILE *stream):从指定流中读取一个字符,并返回其值作为int类型(以便可以区分EOF)。如果到达文件末尾或发生错误,则返回EOF

  2. fputc(int c, FILE *stream):将一个字符写入指定流中,并返回写入的字符。如果发生错误,则返回EOF

  3. getchar(void):从标准输入(通常是键盘)读取一个字符,并返回其值作为int类型。等价于fgetc(stdin)

    c
    #include <stdio.h>
    
    int main() {
        printf("请输入一个字符: ");
        int ch = getchar();
        printf("你输入的字符是: %c\n", ch);
        return 0;
    }
  4. putchar(int c):将一个字符写入到标准输出(通常是屏幕),并返回写入的字符。等价于fputc(c, stdout)

    c
    #include <stdio.h>
    
    int main() {
        char message[] = "Hello, World!";
        for (int i = 0; message[i] != '\0'; ++i) {
            putchar(message[i]);
        }
        putchar('\n'); // 添加换行符
        return 0;
    }
  5. ungetc(int c, FILE *stream):将一个字符推回到流中,使得下一次读取该流时首先读取这个字符。注意,大多数实现只允许一个字符被推回。

    c
    #include <stdio.h>
    
    int main() {
        char ch;
        printf("请输入一个字符: ");
        ch = getchar();
        ungetc(ch, stdin); // 将字符推回到输入流中
    
        ch = getchar(); // 再次读取相同的字符
        printf("你输入的字符是: %c\n", ch);
        return 0;
    }