嵌入式面经-C语言:智能指针,`#define` 和 `const`,`typedef`,头文件中定义静态变量

文章目录

    • C++中的智能指针
    • [`#define` 和 `const` 定义常量的选择](#defineconst` 定义常量的选择)
      • [1. `#define` 是简单的文本替换](#define` 是简单的文本替换)
      • [2. `const` 有类型检查](#2. const 有类型检查)
      • [3. 调试支持](#3. 调试支持)
      • [4. 作用域管理](#4. 作用域管理)
      • [5. 节省内存](#5. 节省内存)
    • [`typedef` 和 `#define` 的区别](#define` 的区别)
      • [1. 类型安全](#1. 类型安全)
      • [2. 指针类型区别](#2. 指针类型区别)
      • [3. 调试和可读性](#3. 调试和可读性)
      • 总结
      • [**`#define` vs `const`**](#definevsconst`**)
      • [**`#define` vs `typedef`**](#definevstypedef`**)
    • 头文件中定义静态变量的问题
      • [1. 每个包含该头文件的源文件都会创建一个独立的静态变量](#1. 每个包含该头文件的源文件都会创建一个独立的静态变量)
      • [2. 可能导致调试困难](#2. 可能导致调试困难)
      • [3. 头文件的正确做法](#3. 头文件的正确做法)
        • [推荐:在头文件中使用 `extern` 声明,而在 `.c` 文件中定义](#推荐:在头文件中使用 extern 声明,而在 .c 文件中定义)
      • [4. `static` 变量的正确使用场景](#4. static 变量的正确使用场景)
      • 结论

C++中的智能指针

普通指针的问题

在C++传统的手动内存管理中,动态分配的对象需要手动释放,否则会产生内存泄露

智能指针的概念

智能指针 (Smart Pointer)是一个类模板,主要用于管理动态分配的堆内存 ,避免手动管理带来的内存泄露、二次释放等问题。智能指针通过RAII(Resource Acquisition Is Initialization) 机制,在对象生命周期结束时自动释放资源,从而提高程序的安全性和可维护性。

C++标准库提供了三种主要的智能指针:

  • std::unique_ptr(独占所有权)
  • std::shared_ptr(共享所有权)
  • std::weak_ptr(弱引用)

智能指针的使用

智能指针通过自动管理资源来解决普通指针的管理问题。

1. std::unique_ptr(独占所有权)
  • unique_ptr 只能有一个指针拥有某块堆内存,不能被复制,只能被移动
  • 适用于独占资源,如文件句柄、互斥锁等。
cpp 复制代码
#include <iostream>
#include <memory> // 需要包含头文件

void func() {
    std::unique_ptr<int> ptr = std::make_unique<int>(10);
    std::cout << *ptr << std::endl; // 输出 10
} // ptr 离开作用域,自动释放内存

int main() {
    func();
    return 0;
}

特点

  • 不能被复制:std::unique_ptr<int> p2 = p1; // 错误
  • 只能转移所有权:std::unique_ptr<int> p2 = std::move(p1);
2. std::shared_ptr(共享所有权)
  • shared_ptr 允许多个 shared_ptr 实例共享同一块堆内存
  • 通过引用计数 (Reference Count)管理资源,只有最后一个 shared_ptr 被销毁时,资源才会被释放。
cpp 复制代码
#include <iostream>
#include <memory>

void func() {
    std::shared_ptr<int> p1 = std::make_shared<int>(20);
    std::shared_ptr<int> p2 = p1; // p2 也共享这块内存
    std::cout << *p1 << ", " << *p2 << std::endl; // 输出 20, 20
} // p1, p2 离开作用域,引用计数变为 0,自动释放内存

int main() {
    func();
    return 0;
}

特点

  • 适用于多个对象共享资源的场景,如缓存、线程池等
  • 通过 use_count() 方法可以查询当前引用计数。

智能指针的内存泄漏

虽然 shared_ptr 自动管理资源,但如果两个 shared_ptr 互相持有对方,会导致循环引用(Cyclic Reference),导致资源无法释放。

循环引用示例
cpp 复制代码
#include <iostream>
#include <memory>

class B; // 先声明

class A {
public:
    std::shared_ptr<B> ptrB;
    ~A() { std::cout << "A 被销毁\n"; }
};

class B {
public:
    std::shared_ptr<A> ptrA;
    ~B() { std::cout << "B 被销毁\n"; }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    
    a->ptrB = b;
    b->ptrA = a;

    // 离开 main() 作用域时,a 和 b 的引用计数都不会变为 0,导致内存泄露
    return 0;
}

问题:

  • ab 互相持有 shared_ptr,导致引用计数始终不为 0,资源不会释放。
  • 解决方案 :使用 std::weak_ptr 破坏循环引用。
3. std::weak_ptr(弱引用,解决循环引用)

weak_ptr 不会增加引用计数 ,它只是一个观察者 ,不会影响 shared_ptr 管理的对象生命周期。

cpp 复制代码
#include <iostream>
#include <memory>

class B; // 先声明

class A {
public:
    std::weak_ptr<B> ptrB; // 改为 weak_ptr
    ~A() { std::cout << "A 被销毁\n"; }
};

class B {
public:
    std::weak_ptr<A> ptrA; // 改为 weak_ptr
    ~B() { std::cout << "B 被销毁\n"; }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    
    a->ptrB = b;
    b->ptrA = a;

    // 现在没有循环引用,资源会被正常释放
    return 0;
}

为什么 weak_ptr 解决了问题?

  • weak_ptr 不增加 shared_ptr 的引用计数,因此不会影响对象的生命周期。
  • weak_ptr 只是一种观察 shared_ptr 是否已经失效,可以通过 lock() 方法获取有效 shared_ptr

总结

智能指针类型 主要特点 适用场景
unique_ptr 独占所有权,不可复制 独占资源,如文件、互斥锁
shared_ptr 共享所有权,引用计数管理 适用于多个对象共享资源
weak_ptr 不增加引用计数,防止循环引用 适用于 shared_ptr 互相引用的情况

智能指针是现代 C++ 的重要工具,能有效减少手动管理内存带来的问题,提高程序的稳定性和安全性。在实际开发中,合理选择合适的智能指针能帮助编写高效、安全、可维护的代码。

#defineconst 定义常量的选择

两者都可以用于定义常量,但 const 通常是更好的选择,原因如下:

1. #define 是简单的文本替换

  • 预处理器会直接将 #define 定义的内容替换到代码中,不进行类型检查。

  • 这可能导致难以发现的错误,例如:

    c 复制代码
    #define PI 3.1415926
    float radius = 2;
    float area = PI * radius * radius;  // 预处理器替换成 3.1415926 * radius * radius
  • 但是如果写成 #define PI 3,1415926(错误的逗号),编译器不会报错,而是会产生奇怪的行为。

2. const 有类型检查

  • const 变量具有类型,编译器会进行类型检查:

    c 复制代码
    const double PI = 3.1415926;
  • 这样可以避免 #define 的文本替换错误,提高代码的可读性和安全性。

3. 调试支持

  • #define 定义的常量在编译时被替换,调试器无法查看它们的值。
  • const 变量存储在内存中,可以在调试时查看其值。

4. 作用域管理

  • #define 没有作用域的概念,定义后在整个代码中都可用,容易引起冲突。
  • const 变量可以限制在特定的作用域,减少潜在错误。

5. 节省内存

  • #define 定义的值会在代码中多次重复,而 const 变量只存储一次,提高内存利用率。

  • 例如:

    c 复制代码
    #define MAX_SIZE 100
    const int MAX_SIZE = 100;
  • #define 版本中,每次使用 MAX_SIZE,编译器都会插入 100,而 const 版本只会存储一份 100,程序会引用该变量。

typedef#define 的区别

两者都可以创建别名,但 typedef 更推荐用于定义数据类型的别名,原因如下:

1. 类型安全

  • #define 只是简单的文本替换,不做类型检查:

    c 复制代码
    #define INT_PTR int *
    INT_PTR a, b; // 实际等价于 `int *a, b;`,b 只是 int 变量
  • typedef 具有类型安全:

    c 复制代码
    typedef int* IntPtr;
    IntPtr a, b;  // `a` 和 `b` 都是 `int*`

2. 指针类型区别

  • typedef 的含义更加清晰:

    c 复制代码
    typedef char* String;
    String s1, s2; // s1 和 s2 都是 `char*`
  • 但是 #define

    c 复制代码
    #define String char*
    String s1, s2; // 实际等价于 `char* s1, s2;`,其中 s2 是 `char` 而不是指针

3. 调试和可读性

  • typedef 允许更好的调试支持,因为它是一个真正的类型定义,而 #define 只是替换文本。

总结

  • 定义常量时 ,优先使用 const 而不是 #define
  • 定义类型别名时 ,使用 typedef 而不是 #define,尤其是在指针和复杂结构时。

#define vs const

对比项 #define const
基本原理 预处理器宏替换,不分配存储空间 变量存储在内存中,有类型信息
类型检查 无类型,不进行类型检查 有类型,编译器进行类型检查
作用域 全局作用域,无作用域概念 受作用域限制(局部或全局)
存储位置 代码段,不占用存储空间 数据段,可能存储在栈、全局区或常量区
调试支持 不能在调试器中查看值 可在调试器中查看
编译方式 预处理阶段替换,无法优化 编译时优化
代码可读性 低,容易引入难以发现的错误 高,更安全,避免潜在错误
示例 #define PI 3.1415926 const double PI = 3.1415926;
推荐使用场景 适用于条件编译、函数宏 适用于定义常量,推荐使用

#define vs typedef

对比项 #define typedef
基本原理 预处理器宏替换,字符串替换 类型定义,创建类型别名
类型检查 无类型检查,可能引入错误 受编译器检查,类型安全
使用方式 只替换文本,可能导致意外错误 定义新的类型,更加清晰
调试支持 无法调试,替换后看不到原始定义 受调试器支持,可查看变量类型
指针定义区别 #define INT_PTR int* 定义的 INT_PTR p1, p2; 只让 p1 成为指针,而 p2 仍然是 int typedef int* IntPtr;IntPtr p1, p2; 都是 int*
结构体定义 需要 #define 结构体名,并手动写 struct 关键字 typedef struct 可直接使用别名
示例 #define INT_PTR int* typedef int* IntPtr;
推荐使用场景 仅适用于简单文本替换 适用于创建类型别名,推荐使用

头文件中定义静态变量的问题

在头文件中定义静态变量static 变量)通常是不推荐的,主要有以下几个原因:

1. 每个包含该头文件的源文件都会创建一个独立的静态变量

  • static 变量的作用域限定在当前编译单元(即当前 .c 文件),如果在头文件中定义 static 变量,那么每个包含该头文件的 .c 文件都会有各自独立的 static 变量副本,而不是共享同一个变量。
  • 这不仅导致 资源浪费 (因为每个 .c 文件都有自己的一份拷贝),还可能导致逻辑错误(多个文件中存在相同名称但不同的变量)。

示例(错误示范):

c 复制代码
// file.h
static int count = 0;  // 头文件中定义静态变量(错误示范)
c 复制代码
// file1.c
#include "file.h"
void func1() {
    count++;  // file1.c 有自己独立的 count 变量
}
c 复制代码
// file2.c
#include "file.h"
void func2() {
    count++;  // file2.c 也有自己独立的 count 变量
}

问题:
file1.cfile2.c 各自有自己的 count 变量,它们不会共享同一个 count,这可能不是我们想要的行为。

2. 可能导致调试困难

由于 static 变量的作用域仅限于当前编译单元,多个 .c 文件中可能会存在多个同名但不同的变量,这会使得代码的可维护性变差,调试时容易造成混淆。

3. 头文件的正确做法

推荐:在头文件中使用 extern 声明,而在 .c 文件中定义

如果变量需要在多个 .c 文件中共享,推荐使用 extern 关键字在头文件中声明,而在某个 .c 文件中定义变量。

示例(正确做法):

c 复制代码
// file.h(仅声明,不定义)
#ifndef FILE_H
#define FILE_H

extern int count;  // 使用 extern 声明全局变量

#endif // FILE_H
c 复制代码
// file.c(定义变量)
#include "file.h"

int count = 0;  // 仅在一个 .c 文件中定义

void increment() {
    count++;
}
c 复制代码
// main.c(使用变量)
#include "file.h"
#include <stdio.h>

int main() {
    printf("count = %d\n", count);
    return 0;
}

这种做法可以确保 count 变量在整个程序中只有一个实例,避免了 static 变量在多个文件中重复定义的问题。

4. static 变量的正确使用场景

尽管不推荐在头文件中定义 static 变量,但它在 .c 文件内部 还是有实际用途的:

  • 限制变量作用域:避免变量在其他文件中被错误访问和修改。
  • 防止命名冲突 :局部 static 变量不会污染全局命名空间。

示例(正确使用 static):

c 复制代码
// file.c
#include <stdio.h>

static int count = 0;  // 仅 file.c 内部可见

void increment() {
    count++;
    printf("count = %d\n", count);
}
c 复制代码
// main.c
#include "file.h"

int main() {
    increment();  // 访问 file.c 内部的静态变量
    return 0;
}

这种 static 变量不会被其他 .c 文件访问,因此是 static 的正确用法。

结论

方案 是否推荐 原因
在头文件中定义 static 变量 ❌ 不推荐 每个包含头文件的 .c 文件都会有独立的变量,浪费资源且容易出错
在头文件中使用 extern 声明,全局变量放在 .c 文件 ✅ 推荐 确保变量在多个 .c 文件中共享,并且只有一个实例
.c 文件中定义 static 变量 ✅ 推荐 限制变量作用域,避免污染全局命名空间

最佳实践:

  • 如果变量需要在多个 .c 文件中共享 → 用 extern 声明,在 .c 文件中定义。
  • 如果变量仅在当前 .c 文件中使用 → 用 static 限制作用域,防止其他文件访问。
  • 避免在头文件中定义 static 变量 ,否则会导致多个 .c 文件各自拥有独立的变量实例,增加内存占用,且可能产生意想不到的错误。
相关推荐
Suwg2091 小时前
【Java导出word】使用poi-tl轻松实现Java导出数据到Word文档
java·开发语言·word·poi-tl
水w1 小时前
【pyCharm Git】根据dev分支新建dev_y分支,本地也新建dev_y分支,并将代码提交到Gitlab上的新分支dev_y上。
开发语言·git·python·pycharm·pull·push·branch
eamon1002 小时前
麒麟V10 arm cpu aarch64 下编译 RocketMQ-Client-CPP 2.2.0
c++·rocketmq·java-rocketmq
范哥来了2 小时前
python 数据可视化matplotib库安装与使用
开发语言·python·信息可视化
laimaxgg2 小时前
Qt窗口控件之颜色对话框QColorDialog
开发语言·前端·c++·qt·命令模式·qt6.3
wkm9562 小时前
Ubuntu Qt: no service found for - “org.qt-project.qt.mediaplayer“
开发语言·qt·ubuntu
小画家~3 小时前
第三:go 操作mysql
开发语言·mysql·golang
修炼成精3 小时前
C#实现的一个简单的软件保护方案
java·开发语言·c#
小麦嵌入式3 小时前
Linux驱动开发实战(六):设备树升级!插件设备树点灯!
linux·c语言·驱动开发·单片机·嵌入式硬件·mcu·ubuntu
乌云暮年3 小时前
算法刷题整理合集(四)
java·开发语言·算法·dfs·bfs