这一部分先从 C++ 的入口知识开始。

如果说 C 更接近“函数 + 数据 + 内存”,那么 C++ 在这个基础上加入了更强的组织能力:命名空间、引用、类、对象、构造析构、继承和多态。第一部分先不急着进入类,先把 C++ 程序的基本框架、输入输出、命名空间和自动类型推断整理清楚。

1.0 C++ 特性

C++ 可以看成是在 C 的基础上增加了面向对象、泛型编程、异常处理、标准库和更强类型系统的语言。

方向 C++ 补充的能力
代码组织 namespace、类、访问权限、成员函数
面向对象 封装、继承、多态、构造函数、析构函数
内存管理 new/delete、RAII、智能指针
函数能力 默认参数、函数重载、内联函数、模板、回调
类型系统 引用、constauto、类型推导
标准库 iostreamstring、容器、算法、迭代器

和 C 的关系

C++ 不是把 C 扔掉,而是保留了大量 C 的语法和底层能力。指针、数组、结构体、函数、内存布局这些内容仍然重要。

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

int main()
{
int value = 10;
int *p = &value;

std::cout << "value = " << value << std::endl;
std::cout << "address = " << p << std::endl;

return 0;
}

后面学 C++ 对象模型时,仍然会回到内存、地址、指针、调用关系这些基础。

C++ 面向对象不是为了“把代码写得高级”,而是为了把数据和行为组织到一起。安全开发里真正要看的,是这些组织方式最后在内存和汇编里呈现成什么样。

1.1 C++ 控制台框架解析

一个最小 C++ 控制台程序通常长这样:

1
2
3
4
5
6
7
#include <iostream>

int main()
{
std::cout << "Hello C++" << std::endl;
return 0;
}

基本组成

代码 作用
#include <iostream> 引入 C++ 标准输入输出流
int main() 程序入口函数
std::cout 标准输出对象
<< 输出运算符
std::endl 换行并刷新输出流
return 0; 程序正常结束

C 风格与 C++ 风格输出

1
2
3
4
5
6
7
8
9
10
11
12
#include <cstdio>
#include <iostream>

int main()
{
int value = 100;

printf("C style: %d\n", value);
std::cout << "C++ style: " << value << std::endl;

return 0;
}

两种风格都能用,但 C++ 里更常见的是 iostream。后续整理时可以把 printf 当成底层和格式化输出复盘,把 cout 当成 C++ 风格输出复盘。

1.2 iostream

iostream 提供 C++ 标准输入输出流。

对象 作用
std::cin 标准输入
std::cout 标准输出
std::cerr 标准错误输出,通常不缓冲
std::clog 日志输出,通常有缓冲

基本输入输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <string>

int main()
{
std::string name;
int age = 0;

std::cout << "name: ";
std::cin >> name;

std::cout << "age: ";
std::cin >> age;

std::cout << "hello, " << name << ", age = " << age << std::endl;

return 0;
}

读取一整行

std::cin >> name 遇到空格会停止。如果要读取一整行,可以用 std::getline

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <string>

int main()
{
std::string line;

std::cout << "input line: ";
std::getline(std::cin, line);

std::cout << "line = " << line << std::endl;

return 0;
}

输入失败检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

int main()
{
int value = 0;

std::cout << "value: ";

if (!(std::cin >> value))
{
std::cerr << "invalid input" << std::endl;
return 1;
}

std::cout << "value = " << value << std::endl;
return 0;
}

C++ 输入输出比 C 风格更方便,但输入仍然不能默认可信。无论是 cin 还是 scanf,都要考虑输入失败、类型不匹配和边界处理。

1.3 namespace

命名空间用于解决命名冲突。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>

namespace math
{
int add(int left, int right)
{
return left + right;
}
}

namespace text
{
int add(int left, int right)
{
return left + right + 100;
}
}

int main()
{
std::cout << math::add(1, 2) << std::endl;
std::cout << text::add(1, 2) << std::endl;

return 0;
}

同样叫 add,放在不同命名空间里,就能区分开。

:: 作用域解析运算符

1
2
math::add(1, 2);
std::cout;

:: 表示到某个作用域里去找名字。

写法 含义
std::cout std 命名空间里的 cout
math::add math 命名空间里的 add
::value 全局作用域里的 value

1.4 using

using 可以减少重复书写命名空间前缀。

1
2
3
4
5
6
7
8
9
10
#include <iostream>

using std::cout;
using std::endl;

int main()
{
cout << "hello" << endl;
return 0;
}

也可以写:

1
using namespace std;

这样可以直接写 coutcinendl

更推荐的习惯

在小练习里写 using namespace std; 很方便,但在正式项目、头文件或多人协作代码里,不建议随便把整个命名空间展开。

1
2
3
// 更精确
using std::cout;
using std::endl;

或者直接保留:

1
std::cout << "hello" << std::endl;

头文件里尤其不要轻易写 using namespace std;。头文件会被很多源文件包含,一旦污染命名空间,后续排查冲突会很麻烦。

1.5 嵌套命名空间

命名空间可以嵌套,用来表达更细的模块层级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

namespace security
{
namespace memory
{
void dump()
{
std::cout << "memory dump" << std::endl;
}
}
}

int main()
{
security::memory::dump();
return 0;
}

C++17 开始可以写得更简洁:

1
2
3
4
5
6
7
namespace security::memory
{
void dump()
{
std::cout << "memory dump" << std::endl;
}
}

适合的命名方式

层级 示例
项目名 ruiqy
模块名 security
子模块 memoryfilenet
具体函数 dumpscanparse

1.6 匿名命名空间

匿名命名空间没有名字,常用于限制当前文件内部可见。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

namespace
{
int file_only_value = 10;

void helper()
{
std::cout << "helper" << std::endl;
}
}

int main()
{
helper();
std::cout << file_only_value << std::endl;
return 0;
}

匿名命名空间里的内容只在当前翻译单元内部可见,适合放一些不希望暴露给其他文件的辅助函数或变量。

static 的关系

C 里常用 static 限制文件作用域;C++ 里匿名命名空间也能达到类似效果。

1
2
3
4
5
6
namespace
{
void local_helper()
{
}
}

对项目来说,这能减少全局符号冲突,也能让模块边界更清楚。

1.7 别名命名空间

命名空间名字太长时,可以起别名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>

namespace ruiqingyan
{
namespace security
{
namespace memory
{
void scan()
{
std::cout << "scan memory" << std::endl;
}
}
}
}

namespace mem = ruiqingyan::security::memory;

int main()
{
mem::scan();
return 0;
}

别名不创建新命名空间,只是给已有命名空间起一个短名字。

使用场景

场景 说明
命名空间太长 提高可读性
第三方库层级深 简化调用
局部文件内频繁使用 减少重复书写

别名要起得清楚,不要短到看不懂。

1.8 合并命名空间

同名命名空间可以分多次定义,最终会被合并。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>

namespace tools
{
void print()
{
std::cout << "print" << std::endl;
}
}

namespace tools
{
void scan()
{
std::cout << "scan" << std::endl;
}
}

int main()
{
tools::print();
tools::scan();
return 0;
}

这使得一个命名空间可以分散在多个文件中。

项目中的意义

1
2
3
4
security/
memory.cpp -> namespace security { ... }
file.cpp -> namespace security { ... }
network.cpp -> namespace security { ... }

多个文件都可以向同一个命名空间添加内容。最终从逻辑上看,它们属于同一组模块。

1.9 自动推断与类型回溯

auto 可以让编译器根据初始化表达式推断变量类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <typeinfo>

int main()
{
auto value = 10;
auto score = 95.5;
auto text = "hello";

std::cout << typeid(value).name() << std::endl;
std::cout << typeid(score).name() << std::endl;
std::cout << typeid(text).name() << std::endl;

return 0;
}

auto 的基本规则

写法 推断结果理解
auto a = 10; int
auto b = 3.14; double
auto c = "hello"; const char *
auto p = &value; 对应类型的指针

引用和 const 要小心

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

int main()
{
int value = 10;
int &ref = value;

auto a = ref;
auto &b = ref;

a = 20;
b = 30;

std::cout << "value = " << value << std::endl;

return 0;
}

auto a = ref; 会复制值,a 是一个新的 intauto &b = ref; 才是引用。

1
2
3
4
5
const int value = 10;

auto a = value; // int,顶层 const 被忽略
const auto b = value; // const int
auto &c = value; // const int&

类型回溯习惯

使用 auto 后,要能回推它到底是什么类型。特别是指针、引用、迭代器、智能指针和模板返回值,不要只因为“能编译”就放过去。

auto 可以减少重复代码,但不能代替理解类型。安全开发和逆向复盘时,类型不清楚,后面的对象布局、指针步长和函数调用都会跟着乱。

本章最小复盘代码

这段代码把 iostream、命名空间、别名和 auto 放在一起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
#include <string>

namespace ruiqy::security
{
struct User
{
std::string name;
int level;
};

void print_user(const User &user)
{
std::cout << "name = " << user.name << std::endl;
std::cout << "level = " << user.level << std::endl;
}
}

namespace sec = ruiqy::security;

int main()
{
sec::User user{"Ruiqy", 3};

auto level = user.level;
auto &name_ref = user.name;

name_ref = "Ruiqingyan";

sec::print_user(user);
std::cout << "copied level = " << level << std::endl;

return 0;
}

复盘时重点看:

  1. sec 只是命名空间别名,不是新命名空间。
  2. sec::User 的完整名字来自 ruiqy::security::User
  3. auto level 是复制出来的 int
  4. auto &name_ref 是对 user.name 的引用。
  5. print_user 使用 const User &,避免复制对象,也避免修改对象。

常见错误整理

错误 示例 问题
头文件滥用 using namespace std; 所有包含者都被污染 容易引发命名冲突
忘记 std:: 直接写 cout 没有 using 时找不到名字
命名空间层级过深 a::b::c::d::func() 可读性下降
别名过短 namespace x = ... 后面读代码不直观
匿名命名空间放公共接口 其他文件无法访问 链接和模块边界混乱
误解 auto 以为一定保留引用和 const 可能发生复制
auto 掩盖复杂类型 指针/引用/智能指针没看清 后续生命周期和修改权限容易错

学习检查清单

检查项 状态
能说出 C++ 相比 C 增加的主要能力 待复盘
能写出最小 C++ 控制台程序 待复盘
能使用 std::coutstd::cinstd::getline 待复盘
能解释 std:::: 的作用 待复盘
能定义普通命名空间并调用其中函数 待复盘
能说明 using namespaceusing std::cout 的区别 待复盘
能写出嵌套命名空间和命名空间别名 待复盘
能说明匿名命名空间适合放什么内容 待复盘
能理解同名命名空间会被合并 待复盘
能使用 auto 并回推真实类型 待复盘
能区分 autoauto &const auto 的差别 待复盘

这一部分的核心是:C++ 开始强调“作用域”和“类型”。命名空间让名字有边界,auto 让类型书写更轻,但复盘时仍然要能把边界和类型说清楚。