应用——C语言基础知识1

一、基本概念

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;                    // 定义结构体类型

主要区别:

  1. 作用时间#define在预处理阶段,typedef在编译阶段

  2. 类型安全typedef更安全,有类型检查

  3. 作用域#define无作用域,typedef有作用域

  4. 使用建议 :定义类型用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;
}

注意事项:

  1. 作用域规则:局部变量优先于全局变量

  2. 访问全局变量 :使用extern声明或使用作用域解析操作符(C不支持::

    复制代码
    extern int count;  // 引用全局变量
  3. 最佳实践

    • 避免重名,提高代码可读性

    • 全局变量加前缀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;
}

内核中使用原因:

  1. 性能要求高:减少函数调用开销

  2. 代码体积小:函数体通常很短

  3. 编译器建议:只是建议,编译器可能忽略

注意事项:

  • 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;
}

调用步骤:

  1. 参数传递:将实参x, y压入栈

  2. 保存现场:保存返回地址和当前寄存器

  3. 跳转:跳转到函数入口地址

  4. 函数执行:执行函数体

  5. 返回值:将结果存入指定寄存器(如eax)

  6. 恢复现场:恢复寄存器,跳回调用处

  7. 清理栈:调用者清理参数空间

栈帧结构:

复制代码
高地址
┌─────────────┐
│  调用者信息   │
├─────────────┤
│   参数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;
}
  • 传递变量的地址

  • 函数内可以通过指针修改原值

  • 适用于大型数据或需要修改的情况

关键区别:

  1. 数据拷贝:值传递拷贝整个数据,指针传递只拷贝地址

  2. 修改原值:值传递不能修改,指针传递可以

  3. 性能:大型数据用指针更高效

  4. 安全性:指针传递有风险(空指针、野指针)

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. 你用过递归吗?

回答:

用过,常见应用场景:

  1. 数学计算:阶乘、斐波那契数列

  2. 数据结构:树的遍历(前序、中序、后序)

  3. 算法:快速排序、归并排序、汉诺塔

  4. 文件系统:目录遍历

示例:阶乘计算

复制代码
// 递归实现
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. 递归函数的优缺点是什么?

回答:

优点:

  1. 代码简洁:适合解决分治问题

  2. 表达力强:更符合问题本质(如树遍历)

  3. 易于理解:对某些问题更直观

缺点:

  1. 性能开销大:函数调用开销,栈空间消耗

  2. 可能栈溢出:深度递归导致栈溢出

  3. 重复计算:如朴素斐波那契递归有大量重复计算

  4. 调试困难:调用层次深时难以调试

示例:斐波那契数列

复制代码
// 朴素递归(效率低,大量重复计算)
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. 如何避免递归中的无限循环?

回答:

预防措施:

  1. 明确递归终止条件

  2. 确保每次递归都向终止条件前进

  3. 添加递归深度限制

  4. 使用迭代替代递归

示例:安全递归函数

复制代码
#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. 数组下标越界会导致什么问题?

回答:

问题表现:

  1. 访问非法内存:读取或修改不属于程序的内存

  2. 程序崩溃:段错误(Segmentation Fault)

  3. 数据损坏:修改其他变量或函数返回地址

  4. 安全漏洞:缓冲区溢出攻击的根源

示例:

复制代码
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语言中的字符串:

  1. 本质 :以'\0'(空字符)结尾的字符数组

  2. 表示:字符数组或字符指针

  3. 特点:不是内置类型,通过标准库函数操作

字符串表示方式:

复制代码
// 方式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. 什么是指针,介绍一下你对指针的理解?

回答:

指针的核心概念:

  1. 本质:存储内存地址的变量

  2. 作用:间接访问和操作数据

  3. 大小:与系统架构相关(32位:4字节,64位:8字节)

比喻理解:

  • 变量:房子(存储数据)

  • 指针:房子的地址(告诉你去哪里找数据)

基本概念:

复制代码
int num = 42;       // 变量,存储值42
int* ptr = &num;    // 指针,存储num的地址

printf("值: %d\n", num);        // 42
printf("地址: %p\n", &num);     // 0x7fff...
printf("指针值: %p\n", ptr);    // 与&num相同
printf("指向的值: %d\n", *ptr); // 42,解引用

指针的重要性:

  1. 动态内存管理malloc/free

  2. 函数参数传递:修改调用者变量

  3. 数据结构:链表、树、图等

  4. 系统编程:访问硬件、内存映射

  5. 函数指针:回调函数、策略模式

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;        // 指向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;        // 危险!未定义行为

初始化最佳实践:

  1. 定义时立即初始化

  2. 暂时不使用时初始化为NULL

  3. 释放后置为NULL

  4. 使用const保护只读数据

3. 什么是地址运算符(&)和解引用运算符(*)?

回答:

地址运算符(&):获取变量地址

复制代码
int num = 42;
int* ptr = &num;  // &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 = &num;

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] → 数组指针
相关推荐
福楠1 天前
模拟实现string类
c语言·开发语言·c++·算法
CC.GG1 天前
【Qt】常用控件----按钮类控件
开发语言·数据库·qt
梨落秋霜1 天前
Python入门篇【序列切片】
开发语言·python
努力努力再努力wz1 天前
2025年度总结:不断迈出第一步
linux·运维·服务器·数据结构·redis·python·django
lbb 小魔仙1 天前
【Linux】嵌入式 Linux 从入门到精通:设备树配置 + 驱动优化核心教程
linux·运维·服务器
小北方城市网1 天前
第 6 课:全栈项目性能 & 安全双进阶 ——Redis 缓存 + JWT 认证(打造高并发高安全后端)
开发语言·数据库·redis·python·安全·缓存·数据库架构
flysh051 天前
C# 核心进阶:深度解析继承(Inheritance)与多态机制
开发语言·c#
kylezhao20191 天前
第二节、C# 上位机核心数据类型详解(工控场景实战版)
开发语言·c#·上位机
Aliex_git1 天前
性能优化 - 构建体积优化
前端·javascript·笔记·学习·性能优化