一、基本概念
1. C语言开发环境是什么?什么平台,什么编辑器,什么编译器?
回答:
-
平台:主要使用Linux/UNIX系统,Windows和嵌入式系统也常用
-
编辑器:常用Vim、VS Code、Sublime Text、Eclipse CDT等
-
编译器:
-
GCC(GNU Compiler Collection):Linux/UNIX下最常用
-
Clang/LLVM:苹果平台常用,编译速度快
-
MSVC:Windows平台Visual Studio的编译器
-
嵌入式专用编译器:如Keil、IAR等,用于ARM、单片机开发
-
2. 为什么用C语言开发,与(如Python、Java)的相比有什么区别?
回答:
C语言特点:
-
编译型语言:直接编译成机器码,执行速度快
-
接近硬件:可以直接操作内存、硬件
-
手动内存管理:效率高但风险大
-
指针操作:灵活但容易出错
与Python/Java对比:
-
性能:C语言性能最好,Python最慢
-
开发效率:Python/Java开发快,C语言开发慢
-
安全性:C语言容易有内存泄漏、缓冲区溢出
-
应用领域:
-
C:操作系统、嵌入式、驱动
-
Python:AI、Web、数据分析
-
Java:企业应用、Android、大型系统
-
3. 如何组织大型C语言项目中的代码(使用makefile怎么管理)?
回答:
代码组织结构:
project/
├── include/ # 头文件
│ ├── utils.h
│ └── data.h
├── src/ # 源文件
│ ├── main.c
│ ├── utils.c
│ └── data.c
├── lib/ # 库文件
├── Makefile # 构建文件
└── README.md
Makefile示例:
makefile
# 编译器和选项
CC = gcc
CFLAGS = -Wall -g -I./include
LDFLAGS = -lm
# 目标文件和源文件
TARGET = myapp
SRCS = $(wildcard src/*.c)
OBJS = $(SRCS:.c=.o)
# 构建规则
$(TARGET): $(OBJS)
$(CC) -o $@ $(OBJS) $(LDFLAGS)
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# 清理
clean:
rm -f $(OBJS) $(TARGET)
# 伪目标
.PHONY: clean
二、基本语法
1. typedef和#define分别有什么作用,有什么区别?
回答:
#define:预处理器指令,简单的文本替换
#define PI 3.14 // 定义常量
#define MAX(a,b) ((a)>(b)?(a):(b)) // 定义宏
#define INT_PTR int* // 定义类型(不推荐)
int* a, b; // a是指针,b是int
INT_PTR c, d; // c是指针,d是int(有歧义!)
typedef:创建类型别名,是编译时行为
typedef int* IntPtr; // 创建类型别名
IntPtr e, f; // e和f都是指针
typedef struct {
int x;
int y;
} Point; // 定义结构体类型
主要区别:
-
作用时间 :
#define在预处理阶段,typedef在编译阶段 -
类型安全 :
typedef更安全,有类型检查 -
作用域 :
#define无作用域,typedef有作用域 -
使用建议 :定义类型用
typedef,定义常量或简单宏用#define
2. 什么是枚举类型?如何定义和使用枚举类型?
回答:
定义枚举:
// 定义方式1:先定义枚举类型,再声明变量
enum Weekday {
MONDAY, // 默认0
TUESDAY, // 1
WEDNESDAY, // 2
THURSDAY, // 3
FRIDAY, // 4
SATURDAY, // 5
SUNDAY // 6
};
enum Weekday today;
// 定义方式2:定义时赋值
enum Color {
RED = 1,
GREEN = 2,
BLUE = 4
};
// 定义方式3:使用typedef简化
typedef enum {
STATE_IDLE,
STATE_RUNNING,
STATE_ERROR
} State;
State current_state;
使用枚举:
enum Color color = RED;
if (color == RED) {
printf("红色\n");
}
// 枚举可以赋值给整数
int code = color; // 1
// 整数可以强制转换为枚举(但类型不安全)
color = (enum Color)2; // GREEN
枚举优点:
-
提高代码可读性
-
有类型检查,比宏安全
-
编译器可以优化
3. 全局变量和局部变量可以重名吗?需要注意什么?
回答:
可以重名,但局部变量会隐藏全局变量:
#include <stdio.h>
int count = 100; // 全局变量
void func() {
int count = 50; // 局部变量,隐藏全局变量
printf("局部count: %d\n", count); // 输出50
{
int count = 20; // 新的局部变量,隐藏外层局部变量
printf("块内count: %d\n", count); // 输出20
}
}
int main() {
printf("全局count: %d\n", count); // 输出100
func();
return 0;
}
注意事项:
-
作用域规则:局部变量优先于全局变量
-
访问全局变量 :使用
extern声明或使用作用域解析操作符(C不支持::)extern int count; // 引用全局变量 -
最佳实践:
-
避免重名,提高代码可读性
-
全局变量加前缀
g_,如g_count -
尽量减少使用全局变量
-
4. if, switch, while, break, continue用法
回答:
if语句:
// 基本形式
if (condition) {
// 条件为真执行
} else if (condition2) {
// 否则如果条件2为真执行
} else {
// 以上都不满足执行
}
// 嵌套if
if (x > 0) {
if (y > 0) {
printf("第一象限\n");
}
}
switch语句:
switch (expression) {
case constant1:
// 代码块
break; // 必须用break,否则会继续执行
case constant2:
// 代码块
break;
default: // 可选
// 默认代码块
break;
}
// 注意:expression必须是整型或枚举类型
// case后面必须是常量表达式
while循环:
// while循环(先判断后执行)
int i = 0;
while (i < 10) {
printf("%d ", i);
i++;
}
// do-while循环(先执行后判断)
int j = 0;
do {
printf("%d ", j);
j++;
} while (j < 10);
break和continue:
// break:跳出当前循环或switch
for (int i = 0; i < 10; i++) {
if (i == 5) {
break; // 跳出循环
}
printf("%d ", i); // 输出0 1 2 3 4
}
// continue:跳过本次循环剩余部分
for (int i = 0; i < 10; i++) {
if (i % 2 == 0) {
continue; // 跳过偶数
}
printf("%d ", i); // 输出1 3 5 7 9
}
// 多层循环中的break
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (j == 1) {
break; // 只跳出内层循环
}
printf("(%d,%d) ", i, j);
}
}
// 输出:(0,0) (1,0) (2,0)
三、函数
1. 手撕strcpy、strcat、strcmp、memcpy(考虑内存重叠问题)
回答:
strcpy实现:
char* my_strcpy(char* dest, const char* src) {
if (dest == NULL || src == NULL) return NULL;
char* ret = dest;
while ((*dest++ = *src++) != '\0');
return ret;
}
strcat实现:
char* my_strcat(char* dest, const char* src) {
if (dest == NULL || src == NULL) return NULL;
char* ret = dest;
// 找到dest的结尾
while (*dest != '\0') dest++;
// 追加src
while ((*dest++ = *src++) != '\0');
return ret;
}
strcmp实现:
int my_strcmp(const char* str1, const char* str2) {
if (str1 == NULL || str2 == NULL) return 0;
while (*str1 && (*str1 == *str2)) {
str1++;
str2++;
}
return *(unsigned char*)str1 - *(unsigned char*)str2;
}
memcpy实现(考虑内存重叠):
void* my_memcpy(void* dest, const void* src, size_t n) {
if (dest == NULL || src == NULL || n == 0) return dest;
char* d = (char*)dest;
const char* s = (const char*)src;
// 检查内存重叠
if (d > s && d < s + n) {
// 从后向前拷贝(处理重叠情况)
d = d + n - 1;
s = s + n - 1;
while (n--) {
*d-- = *s--;
}
} else {
// 从前向后拷贝
while (n--) {
*d++ = *s++;
}
}
return dest;
}
内存重叠示例:
char str[20] = "hello,world";
// 重叠情况:目标地址在源地址范围内
my_memcpy(str + 5, str, 7); // 正常拷贝
// 标准memcpy可能出错,my_memcpy正确处理
2. 带参宏和函数的区别
回答:
区别对比:
| 特性 | 带参宏 | 函数 |
|---|---|---|
| 处理阶段 | 预处理阶段 | 编译/运行阶段 |
| 类型检查 | 无类型检查 | 有类型检查 |
| 调用开销 | 无调用开销(代码展开) | 有调用开销(压栈/弹栈) |
| 调试 | 难以调试 | 容易调试 |
| 代码大小 | 可能增大代码 | 代码复用 |
| 副作用 | 容易有副作用 | 相对安全 |
示例:
// 带参宏
#define SQUARE(x) ((x) * (x))
int a = 5;
int b = SQUARE(a++); // 展开:((a++) * (a++)),a自增两次!
// 函数
int square(int x) {
return x * x;
}
int c = square(a++); // 安全,a只自增一次
3. 内核中出现的inline是什么意思
回答:
inline函数:建议编译器将函数代码直接插入调用处,避免函数调用开销
// 声明inline函数
static inline int max(int a, int b) {
return (a > b) ? a : b;
}
int main() {
int x = 10, y = 20;
int result = max(x, y); // 编译器可能直接展开为:(x > y) ? x : y
return 0;
}
内核中使用原因:
-
性能要求高:减少函数调用开销
-
代码体积小:函数体通常很短
-
编译器建议:只是建议,编译器可能忽略
注意事项:
-
inline只是建议,最终由编译器决定 -
不适合复杂函数,可能导致代码膨胀
-
定义通常在头文件中,加上
static
4. 带参宏实现两个数比较
回答:
// 比较两个数,返回较大值
#define MAX(a, b) ((a) > (b) ? (a) : (b))
// 比较两个数,返回较小值
#define MIN(a, b) ((a) < (b) ? (a) : (b))
// 比较是否相等(考虑浮点数)
#define FLOAT_EQUAL(a, b, eps) (fabs((a) - (b)) < (eps))
// 使用示例
int x = 10, y = 20;
int max_val = MAX(x, y); // 20
int min_val = MIN(x, y); // 10
// 注意:宏参数要加括号,避免运算符优先级问题
#define SQUARE(x) ((x) * (x))
int z = SQUARE(3 + 2); // 正确:((3+2)*(3+2)) = 25
5. 带参宏实现两个数交换
回答:
// 使用临时变量(通用,支持所有类型)
#define SWAP(a, b) do { \
typeof(a) temp = a; \
a = b; \
b = temp; \
} while(0)
// 不使用临时变量(仅限整数)
#define SWAP_INT(a, b) do { \
a = a ^ b; \
b = a ^ b; \
a = a ^ b; \
} while(0)
// 加减法交换(仅限数值类型)
#define SWAP_ADD(a, b) do { \
a = a + b; \
b = a - b; \
a = a - b; \
} while(0)
// 使用示例
int x = 10, y = 20;
SWAP(x, y); // x=20, y=10
// 注意:do{...}while(0)的作用:
// 1. 确保宏在if/else语句中正确使用
// 2. 避免分号问题
if (condition)
SWAP(x, y); // 正确:展开为一个语句块
else
// ...
6. 函数是如何调用的
回答:
函数调用过程:
int add(int a, int b) {
return a + b;
}
int main() {
int x = 5, y = 10;
int result = add(x, y); // 函数调用
return 0;
}
调用步骤:
-
参数传递:将实参x, y压入栈
-
保存现场:保存返回地址和当前寄存器
-
跳转:跳转到函数入口地址
-
函数执行:执行函数体
-
返回值:将结果存入指定寄存器(如eax)
-
恢复现场:恢复寄存器,跳回调用处
-
清理栈:调用者清理参数空间
栈帧结构:
高地址
┌─────────────┐
│ 调用者信息 │
├─────────────┤
│ 参数n │
├─────────────┤
│ ... │
├─────────────┤
│ 参数1 │
├─────────────┤
│ 返回地址 │
├─────────────┤
│ 旧BP/EBP │ ← 当前BP
├─────────────┤
│ 局部变量 │
├─────────────┤
│ 临时空间 │
└─────────────┘
低地址
四、函数参数传递
1. 解释值传递和指针传递的区别
回答:
值传递(Pass by Value):
void change_value(int a) {
a = 100; // 只修改局部副本
}
int main() {
int x = 10;
change_value(x);
printf("%d\n", x); // 输出10,x未改变
return 0;
}
-
传递参数的副本
-
函数内修改不影响原值
-
适用于小型数据
指针传递(Pass by Pointer/Reference):
void change_value_by_pointer(int* p) {
*p = 100; // 修改指针指向的值
}
int main() {
int x = 10;
change_value_by_pointer(&x);
printf("%d\n", x); // 输出100,x被修改
return 0;
}
-
传递变量的地址
-
函数内可以通过指针修改原值
-
适用于大型数据或需要修改的情况
关键区别:
-
数据拷贝:值传递拷贝整个数据,指针传递只拷贝地址
-
修改原值:值传递不能修改,指针传递可以
-
性能:大型数据用指针更高效
-
安全性:指针传递有风险(空指针、野指针)
2. 如何通过指针实现指针传递?
回答:
二级指针示例:
void allocate_memory(int** ptr, int size) {
*ptr = (int*)malloc(size * sizeof(int));
if (*ptr != NULL) {
for (int i = 0; i < size; i++) {
(*ptr)[i] = i; // 注意括号优先级
}
}
}
int main() {
int* array = NULL;
int size = 10;
// 传递指针的地址(二级指针)
allocate_memory(&array, size);
if (array != NULL) {
for (int i = 0; i < size; i++) {
printf("%d ", array[i]);
}
free(array);
}
return 0;
}
为什么要用二级指针:
// 错误:一级指针,无法修改调用者的指针
void wrong_allocate(int* ptr, int size) {
ptr = (int*)malloc(size * sizeof(int)); // 只修改局部副本
}
// 正确:二级指针,可以修改调用者的指针
void correct_allocate(int** ptr, int size) {
*ptr = (int*)malloc(size * sizeof(int)); // 修改指向的内容
}
3. 一个函数传入一个参数,怎么返回两个参数?
回答:
方法1:通过指针参数返回
// 返回两个值:通过指针参数
void get_two_values(int input, int* out1, int* out2) {
*out1 = input * 2; // 第一个返回值
*out2 = input * 3; // 第二个返回值
}
int main() {
int value = 10;
int result1, result2;
get_two_values(value, &result1, &result2);
printf("%d, %d\n", result1, result2); // 20, 30
return 0;
}
方法2:返回结构体
typedef struct {
int first;
int second;
} TwoValues;
TwoValues get_two_values_struct(int input) {
TwoValues result;
result.first = input * 2;
result.second = input * 3;
return result;
}
int main() {
int value = 10;
TwoValues results = get_two_values_struct(value);
printf("%d, %d\n", results.first, results.second); // 20, 30
return 0;
}
方法3:返回指针(需注意内存管理)
int* get_two_values_pointer(int input, int* size) {
int* result = (int*)malloc(2 * sizeof(int));
if (result != NULL) {
result[0] = input * 2;
result[1] = input * 3;
*size = 2;
}
return result; // 调用者需要释放内存
}
五、递归函数
1. 你用过递归吗?
回答:
用过,常见应用场景:
-
数学计算:阶乘、斐波那契数列
-
数据结构:树的遍历(前序、中序、后序)
-
算法:快速排序、归并排序、汉诺塔
-
文件系统:目录遍历
示例:阶乘计算
// 递归实现
unsigned long long factorial_recursive(int n) {
if (n <= 1) return 1;
return n * factorial_recursive(n - 1);
}
// 迭代实现
unsigned long long factorial_iterative(int n) {
unsigned long long result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
return result;
}
2. 递归函数的优缺点是什么?
回答:
优点:
-
代码简洁:适合解决分治问题
-
表达力强:更符合问题本质(如树遍历)
-
易于理解:对某些问题更直观
缺点:
-
性能开销大:函数调用开销,栈空间消耗
-
可能栈溢出:深度递归导致栈溢出
-
重复计算:如朴素斐波那契递归有大量重复计算
-
调试困难:调用层次深时难以调试
示例:斐波那契数列
// 朴素递归(效率低,大量重复计算)
int fib_recursive(int n) {
if (n <= 2) return 1;
return fib_recursive(n-1) + fib_recursive(n-2);
}
// 优化:记忆化搜索
int fib_memo[100] = {0};
int fib_memoization(int n) {
if (n <= 2) return 1;
if (fib_memo[n] != 0) return fib_memo[n];
fib_memo[n] = fib_memoization(n-1) + fib_memoization(n-2);
return fib_memo[n];
}
3. 如何避免递归中的无限循环?
回答:
预防措施:
-
明确递归终止条件
-
确保每次递归都向终止条件前进
-
添加递归深度限制
-
使用迭代替代递归
示例:安全递归函数
#include <stdio.h>
#include <stdbool.h>
#define MAX_DEPTH 1000
// 安全的递归函数
int safe_recursive(int n, int depth) {
// 1. 检查递归深度
if (depth > MAX_DEPTH) {
printf("递归深度超过限制!\n");
return -1;
}
// 2. 明确的终止条件
if (n <= 0) {
return 0;
}
// 3. 确保递归向终止条件前进
return safe_recursive(n - 1, depth + 1) + n;
}
// 迭代替代递归
int iterative_solution(int n) {
int result = 0;
while (n > 0) {
result += n;
n--;
}
return result;
}
int main() {
int result = safe_recursive(10, 0);
printf("结果:%d\n", result);
return 0;
}
常见错误:
// 错误:缺少终止条件
void infinite_loop() {
printf("无限递归...\n");
infinite_loop(); // 没有终止条件
}
// 错误:终止条件永远不会达到
void wrong_condition(int n) {
if (n == 10) return; // 假设是终止条件
wrong_condition(n + 2); // 但n从1开始,1,3,5,7,9,11...永远不会等于10
}
六、数组
1. 数组下标越界会导致什么问题?
回答:
问题表现:
-
访问非法内存:读取或修改不属于程序的内存
-
程序崩溃:段错误(Segmentation Fault)
-
数据损坏:修改其他变量或函数返回地址
-
安全漏洞:缓冲区溢出攻击的根源
示例:
int array[5] = {1, 2, 3, 4, 5};
// 读取越界:可能返回垃圾值
int value = array[10]; // 未定义行为
// 写入越界:可能破坏其他数据
array[10] = 100; // 危险!可能修改其他变量
// 常见错误:循环条件错误
for (int i = 0; i <= 5; i++) { // 应该是 i < 5
array[i] = i; // i=5时越界
}
// 防止越界的方法:
// 1. 使用常量定义数组大小
#define ARRAY_SIZE 5
int arr[ARRAY_SIZE];
for (int i = 0; i < ARRAY_SIZE; i++) {}
// 2. 计算数组元素个数
int arr[] = {1, 2, 3, 4, 5};
int size = sizeof(arr) / sizeof(arr[0]);
2. 多维数组在内存中的存储方式是什么?
回答:
按行存储(Row-major Order):
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
内存布局:
地址: 内容
base: 1 (matrix[0][0])
base+4: 2 (matrix[0][1])
base+8: 3 (matrix[0][2])
base+12: 4 (matrix[0][3])
base+16: 5 (matrix[1][0])
base+20: 6 (matrix[1][1])
...
base+44: 12 (matrix[2][3])
计算元素地址:
c
复制
下载
// 对于int arr[M][N]
// arr[i][j]的地址 = base + (i * N + j) * sizeof(int)
// 示例访问
int value = *(*(matrix + 1) + 2); // matrix[1][2] = 7
int* ptr = &matrix[0][0]; // 指向第一个元素
int seventh = ptr[1 * 4 + 2]; // 等价于matrix[1][2]
验证存储方式:
void print_memory_layout() {
int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
int* p = (int*)arr; // 转换为一级指针
printf("按行存储验证:\n");
for (int i = 0; i < 6; i++) {
printf("arr[%d] = %d\n", i, p[i]);
}
// 输出:1 2 3 4 5 6
}
3. 你知道字符串,描述一下?
回答:
C语言中的字符串:
-
本质 :以
'\0'(空字符)结尾的字符数组 -
表示:字符数组或字符指针
-
特点:不是内置类型,通过标准库函数操作
字符串表示方式:
// 方式1:字符数组
char str1[] = "Hello"; // 自动添加'\0'
char str2[6] = {'H','e','l','l','o','\0'};
// 方式2:字符指针
char* str3 = "Hello"; // 字符串常量,只读
char str4[] = "Hello"; // 可修改的副本
// 字符串长度
int len = strlen(str1); // 5,不包括'\0'
int size = sizeof(str1); // 6,包括'\0'
字符串操作函数:
#include <string.h>
// 复制字符串
char dest[20];
strcpy(dest, "Hello"); // dest = "Hello"
strncpy(dest, "World", 3); // 安全版本
// 连接字符串
strcat(dest, " World"); // dest = "Hello World"
// 比较字符串
int cmp = strcmp("abc", "abd"); // 负值
// 查找字符
char* pos = strchr("Hello", 'l'); // 指向第一个'l'
// 字符串转换
int num = atoi("123"); // 字符串转整数
double val = atof("3.14"); // 字符串转浮点数
字符串安全问题:
// 危险:缓冲区溢出
char buffer[10];
strcpy(buffer, "这是一个很长的字符串"); // 溢出!
// 安全:使用带长度限制的函数
strncpy(buffer, "安全字符串", sizeof(buffer)-1);
buffer[sizeof(buffer)-1] = '\0'; // 确保终止
4. 数组和链表的区别?
回答:
对比表格:
| 特性 | 数组 | 链表 |
|---|---|---|
| 内存分配 | 连续内存块 | 离散内存节点 |
| 大小 | 固定大小(静态) | 动态大小 |
| 访问方式 | 随机访问,O(1) | 顺序访问,O(n) |
| 插入/删除 | O(n),需要移动元素 | O(1),修改指针 |
| 内存效率 | 无额外开销 | 有指针开销 |
| 缓存友好 | 是(局部性原理) | 否 |
| 实现复杂度 | 简单 | 较复杂 |
代码示例:
// 数组
int array[100]; // 固定大小
array[50] = 100; // 直接访问
// 链表节点
typedef struct Node {
int data;
struct Node* next;
} Node;
// 链表操作
Node* create_node(int value) {
Node* node = (Node*)malloc(sizeof(Node));
node->data = value;
node->next = NULL;
return node;
}
// 链表插入
void insert_after(Node* prev, int value) {
Node* new_node = create_node(value);
new_node->next = prev->next;
prev->next = new_node;
}
// 链表删除
void delete_after(Node* prev) {
if (prev->next == NULL) return;
Node* temp = prev->next;
prev->next = temp->next;
free(temp);
}
选择建议:
-
用数组:大小固定、频繁随机访问、追求性能
-
用链表:大小变化大、频繁插入删除、内存受限
七、指针基础
1. 什么是指针,介绍一下你对指针的理解?
回答:
指针的核心概念:
-
本质:存储内存地址的变量
-
作用:间接访问和操作数据
-
大小:与系统架构相关(32位:4字节,64位:8字节)
比喻理解:
-
变量:房子(存储数据)
-
指针:房子的地址(告诉你去哪里找数据)
基本概念:
int num = 42; // 变量,存储值42
int* ptr = # // 指针,存储num的地址
printf("值: %d\n", num); // 42
printf("地址: %p\n", &num); // 0x7fff...
printf("指针值: %p\n", ptr); // 与&num相同
printf("指向的值: %d\n", *ptr); // 42,解引用
指针的重要性:
-
动态内存管理 :
malloc/free -
函数参数传递:修改调用者变量
-
数据结构:链表、树、图等
-
系统编程:访问硬件、内存映射
-
函数指针:回调函数、策略模式
2. 如何定义和初始化一个指针变量?
回答:
定义指针:
// 基本语法:type* pointer_name;
int* int_ptr; // 指向int的指针
char* char_ptr; // 指向char的指针
float* float_ptr; // 指向float的指针
void* void_ptr; // 通用指针,可指向任何类型
// 定义多个指针(注意:*只作用于紧邻的变量)
int *p1, *p2; // 两个指针
int* p3, p4; // p3是指针,p4是int(易错!)
初始化指针:
// 方法1:初始化为NULL(推荐)
int* ptr1 = NULL; // 空指针
// 方法2:指向已有变量
int num = 10;
int* ptr2 = # // 指向num
// 方法3:指向动态分配的内存
int* ptr3 = (int*)malloc(sizeof(int) * 10);
if (ptr3 != NULL) {
// 使用内存
free(ptr3); // 记得释放
}
// 方法4:指向数组
int arr[5] = {1, 2, 3, 4, 5};
int* ptr4 = arr; // 指向数组首元素
int* ptr5 = &arr[2]; // 指向第三个元素
// 方法5:指向字符串常量(只读)
const char* str = "Hello"; // 只读,不能修改内容
// 错误初始化:野指针
int* bad_ptr; // 未初始化,指向随机地址
// *bad_ptr = 10; // 危险!未定义行为
初始化最佳实践:
-
定义时立即初始化
-
暂时不使用时初始化为NULL
-
释放后置为NULL
-
使用const保护只读数据
3. 什么是地址运算符(&)和解引用运算符(*)?
回答:
地址运算符(&):获取变量地址
int num = 42;
int* ptr = # // &num获取num的地址
printf("num的值: %d\n", num); // 42
printf("num的地址: %p\n", &num); // 如0x7ffc...
printf("ptr的值: %p\n", ptr); // 与&num相同
解引用运算符(*):通过地址访问值
int num = 42;
int* ptr = #
printf("直接访问: %d\n", num); // 42
printf("间接访问: %d\n", *ptr); // 42,解引用ptr
// 通过指针修改变量
*ptr = 100; // 等价于 num = 100
printf("修改后: %d\n", num); // 100
运算符优先级示例:
int arr[5] = {1, 2, 3, 4, 5};
int* p = arr;
// 不同写法的等价性
printf("%d\n", arr[2]); // 3
printf("%d\n", *(arr + 2)); // 3,arr
4.空指针是什么?如何使用?
1. 空指针定义
-
空指针:不指向任何有效内存地址的指针
-
在C中通常用
NULL表示(定义为(void*)0)
2. 使用方式
int* ptr = NULL; // 定义并初始化为空指针
// 检查是否为空
if (ptr == NULL) {
printf("指针为空\n");
}
// 函数返回错误时常用
int* allocate_memory(int size) {
if (size <= 0) return NULL; // 错误返回NULL
return malloc(size);
}
5.野指针是什么?如何避免?
1. 野指针定义
-
野指针:指向无效内存地址的指针
-
常见原因:未初始化、已释放、越界访问
2. 如何避免
// 1. 定义时初始化
int* p1 = NULL; // ✅ 初始化为空
// 2. 释放后置空
int* p2 = malloc(sizeof(int));
free(p2);
p2 = NULL; // ✅ 避免野指针
// 3. 不指向局部变量地址(函数返回后失效)
int* bad_ptr() {
int local = 10;
return &local; // ❌ 危险!局部变量会被释放
}
八、指针运算
1. 指针可以进行哪些运算?
int arr[5] = {1, 2, 3, 4, 5};
int* p = arr;
// ✅ 允许的运算:
p++; // 移动到下一个元素
p--; // 移动到上一个元素
p = p + 2; // 向后移动2个元素
p = p - 1; // 向前移动1个元素
int diff = p2 - p1; // 计算两个指针间的元素个数
// ❌ 不允许:
// p * 2; // 不能乘除
// p / 2; // 不能乘除
// p + p2; // 不能相加
2. 如何比较两个指针?
int arr[5] = {1, 2, 3, 4, 5};
int* p1 = &arr[1];
int* p2 = &arr[3];
// 比较地址(需指向同一数组)
if (p1 < p2) { // ✅ p1在p2之前
printf("p1在p2之前\n");
}
if (p1 == p2) { // ✅ 比较是否指向同一地址
printf("指向同一地址\n");
}
// 与NULL比较
int* ptr = NULL;
if (ptr == NULL) { // ✅ 检查是否为空
printf("指针为空\n");
}
九、指针与数组
1. 数组名和指针有什么关系?
int arr[3] = {10, 20, 30};
// 数组名在大多数情况下退化为指向首元素的指针
printf("%p\n", arr); // 数组首地址
printf("%p\n", &arr[0]); // 同上
// 但有两个例外:
printf("%zu\n", sizeof(arr)); // 整个数组大小(12字节)
// sizeof(指针) 是指针本身大小(4或8字节)
int* p = arr;
printf("%zu\n", sizeof(p)); // 指针大小
// &arr 是整个数组的地址(类型为int(*)[3])
printf("%p\n", &arr); // 地址相同但类型不同
2. 如何通过指针访问数组元素?
int arr[5] = {1, 2, 3, 4, 5};
int* p = arr; // 指向第一个元素
// 方法1:下标法(推荐)
printf("%d\n", p[2]); // arr[2]
// 方法2:指针偏移
printf("%d\n", *(p + 2)); // 同上
// 方法3:移动指针
p += 2; // p现在指向arr[2]
printf("%d\n", *p); // 3
// 遍历数组
for (int i = 0; i < 5; i++) {
printf("%d ", *(p + i));
}
3. 指针数组和数组指针的区别是什么?
// 1. 指针数组:元素是指针的数组
int* ptr_array[3]; // 有3个int指针
int a=1, b=2, c=3;
ptr_array[0] = &a;
ptr_array[1] = &b;
ptr_array[2] = &c;
// 2. 数组指针:指向数组的指针
int (*array_ptr)[3]; // 指向有3个int的数组
int arr[3] = {10, 20, 30};
array_ptr = &arr; // 指向整个数组
// 使用区别:
printf("%d\n", *ptr_array[0]); // 访问第一个指针指向的值
printf("%d\n", (*array_ptr)[0]); // 访问数组第一个元素
// 简记:看谁和标识符结合
// int* p[3]; → p[3]是数组,int*是元素类型 → 指针数组
// int (*p)[3]; → *p是指针,指向int[3] → 数组指针