【C++ -Day1】环境搭建与核心语法差异 | 嵌入式 Linux C++ 入门第一课
引言
很多有 5 年以上 Linux C 开发经验的嵌入式工程师,在转向 C++ 时都会陷入一个致命误区:一开始就去啃厚厚的《C++ Primer》,花了几个月时间学了一堆模板元编程、异常处理、RTTI 等工作中 90% 场景根本用不到的高级特性,结果到了实际项目中,连一个简单的类都写不好,甚至还不如用 C 写得顺手。
嵌入式 C++ 的核心哲学是:用最少的特性,解决最实际的问题。
本文作为《嵌入式 Linux C++ 从入门到实战》系列的第一篇,将彻底摒弃学院派的教学方式,直接带你搭建最精简的 Linux C++ 开发环境,只学习 C++ 相比 C 最核心、最刚需的语法差异。我保证,你跟着本文一步步操作,1 小时内就能写出第一个可运行的 C++ 程序,并能将你现有的任意一个 C 小工具无缝改造成 C++ 版本。
前置要求:具备基础的 Linux C 开发经验,了解 gcc 编译、Makefile 基本使用、gdb 基本调试方法。
一、Linux C++ 开发环境搭建(15 分钟)
嵌入式 Linux C++ 开发环境与 C 开发环境高度兼容,你不需要重新安装操作系统或虚拟机,只需要在现有 C 环境基础上补充几个工具即可。
1.1 g++ 编译器安装与基本使用
C++ 程序使用g++编译器编译,而不是 C 语言的gcc。虽然 gcc 也能编译 C++ 代码,但它不会自动链接 C++ 标准库,会导致各种奇怪的链接错误。
Ubuntu/Debian 系统安装:
# 更新软件源
sudo apt update
# 安装完整的C/C++开发工具链
sudo apt install build-essential g++ gdb make -y
# 验证安装
g++ --version
gdb --version
CentOS/RHEL 系统安装:
sudo yum groupinstall "Development Tools"
sudo yum install gcc-c++ gdb make -y
最基础的编译命令:
# 单文件编译
g++ -o hello hello.cpp
# 运行程序
./hello
常用编译选项(与 gcc 基本一致,但有几个 C++ 专属选项):
| 选项 | 说明 | C++ 专属 |
|---|---|---|
-o <file> |
指定输出文件名 | ❌ |
-g |
生成调试信息,用于 gdb 调试 | ❌ |
-O2 |
开启二级优化,嵌入式发布版本必用 | ❌ |
-Wall |
开启所有警告 | ❌ |
-std=c++11 |
指定 C++ 标准版本(推荐 C++11 及以上) | ✅ |
-lstdc++ |
手动链接 C++ 标准库(极少需要) | ✅ |
嵌入式开发最佳实践:统一使用
-std=c++11标准。C++11 是嵌入式领域最成熟、最广泛支持的标准,几乎所有现代交叉编译器都完美支持。C++17 及以上版本在很多旧款嵌入式芯片的编译器中支持不佳。
1.2 C++ 编译链接过程与 C 的区别
很多 C 工程师转 C++ 时遇到的第一个坑就是编译链接错误。虽然整体流程都是 "预处理→编译→汇编→链接",但 C++ 有几个关键差异:
-
名字修饰(Name Mangling)C++ 为了支持函数重载,会对函数名进行修饰。例如
void func(int)会被修饰成_Z4funci,而void func(double)会被修饰成_Z4funcd。这就是为什么 C++ 代码不能直接调用 C 编译的库,反之亦然。 -
全局构造与析构C++ 全局对象的构造函数会在
main函数执行之前调用,析构函数会在main函数执行之后调用。这一点在嵌入式开发中非常重要,因为它会影响系统的启动时间和初始化顺序。 -
标准库链接g++ 会自动链接
libstdc++.so,而 gcc 不会。如果你用 gcc 编译 C++ 代码,必须手动加上-lstdc++选项。
1.3 gdb 调试 C++ 程序的特殊技巧
gdb 完全支持 C++ 调试,但有几个针对 C++ 特性的特殊命令,是 C 工程师必须掌握的:
1. 查看类成员变量
gdb
# 假设我们有一个类Person,实例化了对象p
(gdb) p p # 直接打印对象,会显示所有成员变量
$1 = {name = "Zhang San", age = 25, id = 1001}
# 打印单个成员变量
(gdb) p p.age
$2 = 25
2. 在类成员函数上设置断点
gdb
# 方式1:类名::函数名
(gdb) b Person::showInfo
Breakpoint 1 at 0x4008a6: file hello.cpp, line 15.
# 方式2:文件名:行号(推荐,更直观)
(gdb) b hello.cpp:15
3. 查看虚函数表(进阶)
gdb
# 查看对象的虚函数表指针
(gdb) p *(void**)p
$3 = (void *) 0x400c80 <vtable for Person+16>
# 查看虚函数表内容
(gdb) x/3a 0x400c80
0x400c80 <_ZTV6Person+16>: 0x4008a6 <Person::showInfo()>
0x400c88 <_ZTV6Person+24>: 0x4008d2 <Person::~Person()>
嵌入式调试注意事项:如果调试交叉编译的程序,需要使用对应架构的 gdb(如
arm-linux-gnueabihf-gdb),并且确保编译时加上了-g选项。
二、命名空间 namespace(10 分钟)
命名空间是 C++ 相比 C 最基础也是最重要的特性之一,它的唯一目的就是解决大型工程中的命名冲突问题。
2.1 为什么需要命名空间
在 C 语言中,所有全局变量和函数都在同一个全局命名空间中。当工程规模超过 10 万行代码,或者集成多个第三方库时,命名冲突几乎是不可避免的。
例如:
// 模块A:uart.c
void init() {
// 初始化串口
}
// 模块B:gpio.c
void init() {
// 初始化GPIO
}
这两个init函数在链接时会报 "多重定义" 错误。在 C 语言中,我们只能通过给函数名加前缀来解决:uart_init()、gpio_init()。但这种方式非常丑陋,而且当前缀越来越长时,代码可读性会急剧下降。
C++ 的命名空间完美解决了这个问题:
// 模块A:uart.cpp
namespace uart {
void init() {
// 初始化串口
}
}
// 模块B:gpio.cpp
namespace gpio {
void init() {
// 初始化GPIO
}
}
// 调用
int main() {
uart::init(); // 调用串口初始化
gpio::init(); // 调用GPIO初始化
return 0;
}
2.2 std 标准命名空间的正确使用方式
C++ 标准库的所有内容都定义在std命名空间中。很多新手喜欢在代码开头写using namespace std;,这是一个非常不好的习惯,尤其是在大型工程中。
错误用法(不推荐):
#include <iostream>
using namespace std; // 污染全局命名空间
int main() {
cout << "Hello World" << endl;
return 0;
}
正确用法(推荐):
#include <iostream>
int main() {
std::cout << "Hello World" << std::endl;
return 0;
}
折中用法(在 cpp 文件中使用):
#include <iostream>
// 只在当前文件中引入std命名空间,不会影响其他文件
using std::cout;
using std::endl;
int main() {
cout << "Hello World" << endl;
return 0;
}
绝对禁止:在头文件中使用
using namespace std;!这会导致所有包含这个头文件的代码都被污染,引发难以排查的命名冲突。
2.3 嵌入式工程中命名空间的命名规范
在嵌入式开发中,我们通常按照以下规则命名命名空间:
- 一级命名空间:公司或项目名称(如
mycompany、smartlock) - 二级命名空间:模块名称(如
uart、gpio、display) - 三级命名空间:子模块或功能(如
uart::debug、gpio::led)
示例:
namespace smartlock {
namespace uart {
void init(int baudrate);
void send(const char* data, int len);
}
namespace gpio {
namespace led {
void on(int id);
void off(int id);
}
}
}
// 调用
smartlock::uart::init(115200);
smartlock::gpio::led::on(0);
三、引用 &(嵌入式开发必备)(15 分钟)
引用是 C++ 提供的一个语法糖,它本质上是一个 "变量的别名"。在嵌入式开发中,引用的使用频率甚至超过了指针,是必须掌握的核心特性。
3.1 引用与指针的本质区别
很多 C 工程师会把引用和指针混为一谈,但它们有本质的区别:
| 特性 | 指针 | 引用 |
|---|---|---|
| 定义 | 存储变量地址的变量 | 变量的别名 |
| 空值 | 可以为 NULL | 不能为 NULL,必须初始化 |
| 重新赋值 | 可以指向其他变量 | 不能,永远指向初始化时的变量 |
| 内存占用 | 占用 4/8 字节(与架构有关) | 不占用额外内存 |
| 多级 | 支持多级指针(int**) |
不支持多级引用 |
代码示例:
int a = 10;
// 指针
int* p = &a;
*p = 20; // 解引用才能修改a的值
// 引用
int& ref = a; // ref是a的别名
ref = 30; // 直接修改a的值,不需要解引用
std::cout << a << std::endl; // 输出30
3.2 引用替代二级指针作为函数出参
在 C 语言中,当我们需要函数返回多个值时,通常使用指针作为出参。如果需要返回一个指针类型的值,就必须使用二级指针,这非常容易出错。
C 语言写法(二级指针):
// 分配内存,通过二级指针返回
void allocate_memory(int** ptr, int size) {
*ptr = (int*)malloc(size * sizeof(int));
}
int main() {
int* arr = NULL;
allocate_memory(&arr, 10); // 必须传地址
// 使用arr
free(arr);
return 0;
}
C++ 写法(引用):
// 分配内存,通过指针引用返回
void allocate_memory(int*& ptr, int size) {
ptr = new int[size];
}
int main() {
int* arr = NULL;
allocate_memory(arr, 10); // 直接传变量,不需要取地址
// 使用arr
delete[] arr;
return 0;
}
可以看到,引用的写法更加简洁直观,而且避免了二级指针容易出现的解引用错误。
3.3 引用在函数参数传递中的优势
在 C 语言中,函数参数传递默认是值传递。当传递一个大的结构体时,会发生一次完整的拷贝,这在资源受限的嵌入式系统中是非常昂贵的。
C 语言写法(值传递,有拷贝):
typedef struct {
int id;
char name[32];
float temperature;
float humidity;
} SensorData;
// 传递结构体,发生拷贝
void print_sensor_data(SensorData data) {
printf("ID: %d, Temp: %.2f, Hum: %.2f\n", data.id, data.temperature, data.humidity);
}
C++ 写法(const 引用,无拷贝):
struct SensorData {
int id;
char name[32];
float temperature;
float humidity;
};
// const引用传递,无拷贝,同时防止数据被修改
void print_sensor_data(const SensorData& data) {
std::cout << "ID: " << data.id
<< ", Temp: " << data.temperature
<< ", Hum: " << data.humidity << std::endl;
}
嵌入式开发最佳实践:所有非基本类型(结构体、类、数组)的函数参数,都应该使用
const引用传递。这可以避免不必要的拷贝,提高程序运行效率。
四、const 高阶用法(15 分钟)
const 是 C++ 中最强大的关键字之一,它的用法比 C 语言丰富得多。在嵌入式开发中,const 不仅可以提高代码的可读性和可维护性,还可以帮助编译器进行更好的优化,甚至可以保护硬件寄存器不被误修改。
4.1 const 修饰变量(替代 #define 定义常量)
在 C 语言中,我们通常使用#define来定义常量,但#define是预处理指令,没有类型检查,容易出错。C++ 推荐使用const来定义常量。
C 语言写法:
#define MAX_BUFFER_SIZE 1024
#define PI 3.1415926
C++ 写法:
const int MAX_BUFFER_SIZE = 1024;
const double PI = 3.1415926;
const常量相比#define的优势:
- 有类型检查,编译器会在编译时发现类型不匹配的错误
- 有作用域限制,可以定义在函数内部或命名空间中
- 可以取地址,而
#define不能 - 支持复杂类型(如结构体、类)
4.2 const 修饰函数参数
当我们不希望函数修改传入的参数时,应该用const修饰它。这不仅可以防止参数被误修改,还可以让函数接受const类型的参数。
// 错误:不能修改const参数
void bad_func(const int& x) {
x = 10; // 编译错误
}
// 正确:只读访问参数
void good_func(const int& x) {
std::cout << x << std::endl;
}
4.3 const 修饰函数返回值
当函数返回一个引用或指针时,如果不希望返回值被修改,应该用const修饰返回值。
class Array {
private:
int data[10];
public:
// 返回const引用,防止外部修改数组元素
const int& operator[](int index) const {
return data[index];
}
};
int main() {
Array arr;
std::cout << arr[0] << std::endl; // 正确
arr[0] = 10; // 编译错误,不能修改const引用
return 0;
}
4.4 const 在嵌入式硬件编程中的应用
这是 const 在嵌入式开发中最有价值的用法之一。我们可以用const来修饰指向硬件寄存器的指针,保护只读寄存器不被误写。
// 定义GPIO寄存器地址
#define GPIOA_BASE 0x40020000
// GPIOA输入数据寄存器(只读)
const volatile uint32_t* const GPIOA_IDR = (const volatile uint32_t*) (GPIOA_BASE + 0x10);
// GPIOA输出数据寄存器(可写)
volatile uint32_t* const GPIOA_ODR = (volatile uint32_t*) (GPIOA_BASE + 0x14);
int main()
{
// 正确:读取输入寄存器
uint32_t input = *GPIOA_IDR;
// 错误:不能写入只读寄存器,编译时就会报错
*GPIOA_IDR = 0x1234; // 编译错误
// 正确:写入输出寄存器
*GPIOA_ODR = 0x0001;
return 0;
}
在这个例子中,GPIOA_IDR被定义为const volatile uint32_t* const,这意味着:
- 第一个
const:指针指向的内容是只读的,不能修改 volatile:告诉编译器不要优化对这个地址的访问- 最后一个
const:指针本身是只读的,不能指向其他地址
这种写法可以在编译阶段就防止对只读寄存器的误写,避免了运行时难以排查的硬件错误。
五、实战:将 C 语言 Hello World 改造成 C++ 版本(5 分钟)
现在,让我们把一个经典的 C 语言 Hello World 程序改造成 C++ 版本,巩固今天所学的知识。
C 语言版本:
#include <stdio.h>
#include <stdlib.h>
#define MAX_NAME_LEN 32
typedef struct {
char name[MAX_NAME_LEN];
int age;
} Person;
void print_person(const Person* p)
{
printf("Name: %s, Age: %d\n", p->name, p->age);
}
int main()
{
Person p;
snprintf(p.name, MAX_NAME_LEN, "Zhang San");
p.age = 25;
print_person(&p);
return 0;
}
C++ 版本:
#include <iostream>
#include <cstring> // C标准库头文件在C++中加c前缀
const int MAX_NAME_LEN = 32; // 用const替代#define
struct Person
{
char name[MAX_NAME_LEN];
int age;
};
// 用const引用替代const指针
void print_person(const Person& p)
{
std::cout << "Name: " << p.name << ", Age: " << p.age << std::endl;
}
int main()
{
Person p;
strncpy(p.name, "Zhang San", MAX_NAME_LEN);
p.age = 25;
print_person(p); // 直接传对象,不需要取地址
return 0;
}
编译运行:
g++ -std=c++11 -o hello hello.cpp
./hello
# 输出:Name: Zhang San, Age: 25
恭喜你!你已经成功写出了第一个 C++ 程序。这个程序虽然简单,但已经用到了今天学习的所有核心知识点:g++ 编译、const 常量、引用、std 命名空间。
六、今日总结与作业
今日核心知识点总结
- 开发环境:使用 g++ 编译 C++ 程序,推荐使用 C++11 标准
- 命名空间:解决命名冲突问题,禁止在头文件中使用
using namespace std - 引用:变量的别名,替代二级指针作为出参,用于函数参数传递避免拷贝
- const:替代 #define 定义常量,修饰函数参数和返回值,保护只读硬件寄存器
今日作业
- 将你最近写的一个 C 语言小工具(如串口调试、LED 控制)改造成 C++ 版本
- 尝试使用 gdb 调试你的 C++ 程序,设置断点并查看变量值
- 思考:在你的项目中,哪些地方可以用引用替代指针来提高代码可读性
下一篇预告
下一篇文章: << 【C++ -Day2】C++ 函数与内存管理 | 彻底告别 char * 字符串处理>>
原创不易,如果本文对你有帮助,欢迎点赞、收藏、关注三连!有任何问题都可以在评论区留言,我会及时回复。
更多推荐
所有评论(0)