数据类型与变量
(1)变量是内存中的一个存储区域,该区域的数据可以在同一类型范围内不断变化。 (2)通过变量名,可以引用这块内存区域,获取里面存储的值。 (3)变量的构成包含三个要素:数据类型、变量名、存储的值。
变量必须先声明,后使用。可以先声明变量再赋值,也可以在声明变量的同时进行赋值。
C语言中变量、函数、数组名、结构体等要素命名时使用的字符序列,称为标识符
- 标识符由字母、数字和下划线组成,且第一个字符必须是字母或下划线。
- 标识符区分大小写,长度限制取决于编译器和目标平台的具体实现
- 标识符不能是C语言的关键字。
- 标识符命名应具有描述性,便于理解和维护。
- 函数不返回任何值:
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
:扩展整数长度
unsigned char byte = 255; // 0 到 255
signed short temperature = -30;
unsigned long population = 7800000000;
C99新增类型: 固定宽度整数类型
#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 |
float pi = 3.14159f;
double atomic_mass = 1.66053906660e-27;
long double very_precise = 3.141592653589793238L;
C99新增类型: 复数类型
#include <complex.h>
double complex z = 1.0 + 2.0 * I;
数据类型转换
1. 隐式类型转换
自动发生的转换,遵循类型提升规则
int i = 10;
float f = 3.14;
double d = i + f; // i转换为float,然后结果转换为double
2. 显式类型转换(强制转换)
double x = 3.14159;
int y = (int)x; // y = 3
int a = 10, b = 3;
float result = (float)a / b; // 3.333...
强制转换可能导致数据丢失,应谨慎使用。
布尔类型
C99标准引入的布尔类型(_Bool
或bool
)本质上仍然是一种整数类型,其核心特点是:
- 存储形式:实际仍以整数值存储(通常1字节)
- 取值规范:
0
表示假(false
),任何非0
值表示真(true
) - 类型安全:相比传统方式,提供了更明确的语义标记
布尔类型的底层实现:
// C标准中的定义(通常实现方式)
#define bool _Bool
#define true 1
#define false 0
布尔变量在内存中仍占用至少1字节空间,但逻辑上只应包含0或1。
在C99标准引入<stdbool.h>
之前,开发者使用多种方式表示布尔值:
1. 直接使用整数
int is_ready = 1; // 真
int is_empty = 0; // 假
2. 自定义宏定义
#define TRUE 1
#define FALSE 0
typedef int BOOL;
BOOL flag = TRUE;
3. 枚举类型
typedef enum { false, true } bool;
bool file_exists = true;
4. 位字段(结构体位域)
struct {
unsigned int is_active : 1; // 只使用1位
} status;
status.is_active = 1;
#include <stdbool.h>
该头文件提供:
bool
:布尔类型别名(实际为_Bool
)true
:值为1的常量false
:值为0的常量
bool is_raining = true;
bool is_sunny = false;
if (is_raining) {
printf("Take an umbrella\n");
}
内存占用:
printf("%zu\n", sizeof(bool)); // 通常是1(但标准只要求至少1)
与旧代码的兼容
// 新旧布尔类型混用时的安全写法
#ifdef __STDC_VERSION__
#include <stdbool.h>
#else
typedef enum { false, true } bool;
#endif
常量的定义
在C语言中,常量是程序运行期间不可改变的值。C语言提供了两种主要方式来定义常量:使用预处理指令#define
和使用const
关键字。
常量是程序中固定不变的值,与变量相对。使用常量的好处包括:
- 提高代码可读性(如
PI
比3.14159
更易理解) - 便于统一修改(只需修改一处定义)
- 避免意外修改导致错误
#define
宏定义常量
- 预处理阶段替换:在编译前由预处理器进行文本替换
- 无类型检查:只是简单的文本替换,不进行类型验证
- 不分配内存:因为只是文本替换,没有存储概念
- 作用域:从定义处到文件末尾,或直到
#undef
- 常见用途:定义数值常量、字符串常量、条件编译
#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语言的作用域规则(块作用域、文件作用域等)
- 常见用途:需要类型检查的常量、数组大小、函数参数
#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;
}
#define
与 const
的关键区别
特性 | #define | const |
---|---|---|
处理阶段 | 预处理阶段(文本替换) | 编译阶段 |
类型检查 | 无类型 | 有类型,编译器会检查 |
内存分配 | 不分配内存 | 通常分配只读内存 |
作用域 | 文件作用域(可被#undef取消) | 遵循C语言作用域规则 |
调试可见性 | 调试器中不可见(已被替换) | 调试器中可见 |
数组大小定义 | 可用于静态数组大小 | C89中不能用于静态数组大小 |
指针使用 | 不能定义指向常量的指针 | 可以定义指向常量的指针 |
复合类型 | 不能定义结构体等复合类型常量 | 可以定义复合类型常量 |
#define
的高级用法
// 带参数的宏
#define MAX(a,b) ((a) > (b) ? (a) : (b))
// 字符串化
#define STR(x) #x
printf("%s\n", STR(Hello)); // 输出 "Hello"
// 符号连接
#define CONCAT(a,b) a##b
int CONCAT(var,1) = 10; // 相当于 int var1 = 10;
const
的高级用法
// 指针常量
const char *p1 = "Hello"; // 指向常量的指针
char *const p2 = "World"; // 常量指针
// 结构体常量
const struct Point {
int x;
int y;
} origin = {0, 0};
// 常量数组
const int days[] = {31, 28, 31, 30, 31};
注意事项
-
宏的副作用:
c#define SQUARE(x) x * x int result = SQUARE(2+3); // 展开为 2+3 * 2+3 = 11,不是25 // 应改为: #define SQUARE(x) ((x) * (x))
-
C与C++的区别: 在C++中,
const
常量默认有内部链接(可通过extern
改变), C++中const
可以用于数组大小定义 -
存储位置:
const
变量通常存储在只读数据段(取决于实现),某些嵌入式系统中可能存储在Flash而非RAM
现代C的最佳实践——对于C99及以上版本的项目:
// 现代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
int a = 10, b = 20;
int max = (a > b) ? a : b; // 如果 a > b,max = a;否则 max = b
printf("Max: %d\n", max); // 输出:Max: 20
其他运算符
-
逗号运算符 (
,
):- 用于将多个表达式连接在一起,整个表达式的值是最后一个表达式的值。
- 示例:
c
int a = (1, 2, 3); // a = 3
-
取地址运算符 (
&
) 和 指针解引用运算符 (*
):- 用于操作内存地址。
- 示例:
c
int x = 10; int *p = &x; // p 存储 x 的地址 printf("%d\n", *p); // 输出:10
-
sizeof 运算符:
- 返回变量或数据类型的大小(以字节为单位)。
- 示例:
c
int size = sizeof(int); // size = 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语言中的程序控制语句用于控制程序的执行流程,包括条件判断、循环和跳转等。这些控制语句使得程序可以根据不同的条件执行不同的代码块,或者重复执行某些操作,从而实现复杂的功能。
- 条件语句:
if
、if-else
、if-else if-else
和switch
。 - 循环语句:
for
、while
和do-while
。 - 跳转语句:
break
、continue
、goto
和return
。
合理使用这些控制语句,可以使你的代码更加清晰、高效和易于维护。同时,建议避免过度使用 goto
,以保持代码的可读性和结构化风格。
条件语句
if
语句: 用于在满足某个条件时执行特定代码块。
int x = 10;
if (x > 5) {
printf("x is greater than 5\n");
}
if-else
语句: 用于在条件为真时执行一个代码块,否则执行另一个代码块。
int x = 3;
if (x > 5) {
printf("x is greater than 5\n");
} else {
printf("x is less than or equal to 5\n");
}
if-else if-else
语句: 当需要检查多个条件时,可以使用if-else if-else
结构。
int score = 85;
if (score >= 90) {
printf("Grade: A\n");
} else if (score >= 75) {
printf("Grade: B\n");
} else {
printf("Grade: C\n");
}
switch
语句:用于基于变量的值执行多个代码块之一。
- 每个
case
后面必须有break
,否则会继续执行后续的case
代码(称为“贯穿”)。 default
是可选的,用于处理没有匹配的case
的情况。
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");
}
循环语句
for
循环: 用于重复执行一段代码固定次数或基于某个条件。
for (int i = 1; i <= 5; i++) {
printf("%d ", i);
}
// 输出:1 2 3 4 5
while
循环: 在条件为真时重复执行代码块。
int i = 1;
while (i <= 5) {
printf("%d ", i);
i++;
}
// 输出:1 2 3 4 5
do-while
循环:至少执行一次循环体,然后在条件为真时继续执行。
int i = 1;
do {
printf("%d ", i);
i++;
} while (i <= 5);
// 输出:1 2 3 4 5
跳转语句
break
:立即退出当前循环或switch
语句。
for (int i = 1; i <= 10; i++) {
if (i == 5) {
break; // 退出循环
}
printf("%d ", i);
}
// 输出:1 2 3 4
continue
:用于跳过当前迭代的剩余部分,直接进入下一次迭代。
for (int i = 1; i <= 5; i++) {
if (i == 3) {
continue; // 跳过 i == 3 的迭代
}
printf("%d ", i);
}
// 输出:1 2 4 5
goto
:用于无条件跳转到指定标签处。虽然不推荐频繁使用,但在某些情况下(如错误处理)可能有用。
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.
return
: 用于从函数中返回值,并结束函数的执行。
int add(int a, int b) {
return a + b;
}
C语言标准输入输出
C语言的标准输入输出功能由stdio.h
头文件提供的一系列函数支持。这些函数提供了与用户进行交互的能力,允许程序读取用户的输入或将信息输出给用户
C语言的I/O设计基于流(stream)的概念,为各种设备提供了统一的接口.
流(Stream): 抽象数据源/目的地:流是对输入输出设备的抽象。主要有两种基本类型:
- 文本流:由字符组成的序列
- 二进制流:由原始字节组成的序列
常见预定义流:stdin:标准输入流(键盘)
, stdout:标准输出流(屏幕)
, stderr:标准错误流(屏幕)
printf() 函数
printf() 函数 用于格式化输出数据到终端或指定的输出流, int printf(const char *format, ...);
#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* 流指针。
fprintf():类似于 printf(),但允许指定不同的流
FILE *file = fopen("example.txt", "w");
if (file != NULL) {
fprintf(file, "Hello, file!\n");
fclose(file);
}
scanf() 函数
scanf() 函数用于从标准输入(通常是键盘)读取数据并根据指定的格式存储到变量中:
int scanf(const char *format, ...);
#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* 流指针。
fscanf():类似于 scanf(),但可以从指定的流中读取数据
FILE *file = fopen("example.txt", "r");
if (file != NULL) {
char buffer[100];
fscanf(file, "%s", buffer);
printf("Read from file: %s\n", buffer);
fclose(file);
}
行 I/O 函数
gets(char *str)
和puts(const char *str)
gets(char *str)
: 从标准输入读取一行文本并存储到指定的缓冲区str
中,直到遇到换行符为止。不推荐使用。- 安全问题:没有提供缓冲区大小限制,容易导致缓冲区溢出攻击,非常不安全。已经被C11标准弃用。
- 替代方案:使用
fgets(str, size, stdin)
代替gets(str)
。
puts(const char *str)
: 向标准输出打印一个字符串,并自动追加一个换行符。不如fputs()
灵活,因为它总是添加换行符且只能输出到标准输出。cputs("Hello, World!");
fgets(char *str, int n, FILE *stream)
和fputs(const char *str, FILE *stream)
fgets
:从指定流中读取最多n-1
个字符(或直到遇到换行符\n
)到缓冲区str
,并在末尾添加一个空字符\0
。相比gets()
,它允许指定缓冲区大小,从而避免了缓冲区溢出的风险。cchar buffer[100]; if (fgets(buffer, sizeof(buffer), stdin) != NULL) { printf("You entered: %s", buffer); }
fputs
:将字符串写入指定流中。与puts()
不同的是,它不会自动添加换行符。适用于需要精确控制输出的情况。
cfputs("Hello, World!\n", stdout);
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()
,但是可以指定分隔符而不是默认的换行符。
- 避免使用
gets()
:由于缺乏对缓冲区大小的检查,可能导致缓冲区溢出,这是严重的安全隐患。应始终使用fgets()
作为替代。 - 注意缓冲区大小:当使用
fgets()
时,确保传递正确的缓冲区大小参数以防止溢出。例如,如果缓冲区大小为100,则应该调用fgets(buffer, 100, stdin)
。 - 错误处理:无论是使用
fgets()
还是其他I/O函数,都应该检查返回值,以便正确处理可能发生的错误情况。例如,fgets()
在到达文件末尾或发生错误时会返回NULL
。
字符 I/O 函数
字符I/O函数在C语言中用于处理单个字符的读写操作,它们提供了比格式化输入输出(如printf()
和scanf()
)更细粒度的控制。这些函数特别适用于需要逐字符处理文本的应用场景。
-
fgetc(FILE *stream)
:从指定流中读取一个字符,并返回其值作为int
类型(以便可以区分EOF)。如果到达文件末尾或发生错误,则返回EOF
。 -
fputc(int c, FILE *stream)
:将一个字符写入指定流中,并返回写入的字符。如果发生错误,则返回EOF
。 -
getchar(void)
:从标准输入(通常是键盘)读取一个字符,并返回其值作为int
类型。等价于fgetc(stdin)
。c#include <stdio.h> int main() { printf("请输入一个字符: "); int ch = getchar(); printf("你输入的字符是: %c\n", ch); return 0; }
-
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; }
-
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; }