编码 · 编程语言
C++ 入门教程(零基础)
简短回答
C++ 是一门编译型、静态类型的语言,把 C 的速度和底层控制能力与类、模板和庞大的标准库结合在一起。用 g++ -std=c++20 hello.cpp -o hello 编译 hello.cpp,再运行 ./hello。现代 C++ 代码使用 std::string、std::vector 和 std::map,而不是原始数组;使用智能指针,而不是手动 delete;并借助 RAII 让资源自动释放。
C++ 是什么,用来做什么
C++ 由 Bjarne Stroustrup 在贝尔实验室创建,1979 年起步,当时叫“带类的 C”。它由 ISO 标准化;C++11 开启了“现代 C++”时代,随后是 C++17、C++20 和 C++23。C++ 用于性能和控制力至关重要的场景:游戏引擎(Unreal Engine)、浏览器(Chrome、Firefox)、数据库、交易系统、嵌入式设备、编译器和机器学习运行时。它的指导思想是零开销抽象:你没用到的特性不会带来任何代价。
安装编译器并运行 Hello, world
在 Linux 和 macOS 上使用 GCC(g++)或 Clang(clang++),在 Windows 上通过 Visual Studio 使用 Microsoft Visual C++。请显式指定标准版本。
#include <iostream>
int main() {
std::cout << "Hello, world\n";
return 0;
}g++ -std=c++20 -Wall -Wextra hello.cpp -o hello
./hello
# Hello, worldstd::cout 是标准输出流,<< 向它写入内容。标准库中的所有内容都位于命名空间 std 中。
变量、类型与 auto
C++ 拥有与 C 相同的内置类型,另外 bool 是关键字,还有引用以及 std::string 等库类型。auto 让编译器根据初始化表达式推导类型。花括号初始化 {} 会拒绝窄化转换。
| 类型 | 示例 | 说明 |
|---|---|---|
int, long long | int n{42}; | 有符号整数 |
double | double x{3.14}; | 64 位浮点数 |
bool | bool ok{true}; | true 或 false |
char | char c{'A'}; | 一个字节 |
std::string | std::string s{"hi"}; | 可增长的文本,来自 <string> |
auto | auto v = 2.5; | 自动推导,这里是 double |
T& | int& r = n; | 引用:n 的另一个名字 |
#include <iostream>
#include <string>
int main() {
int count{7};
auto avg = count / 2.0; // double, 3.5
std::string name{"Ada"};
name += " Lovelace";
const int& ref = count; // read-only alias of count
std::cout << name << " " << name.size() << " " << avg << " " << ref << "\n";
// Ada Lovelace 12 3.5 7
}流程控制与函数
流程控制与 C 相同,另外还有基于范围的 for 循环。传递大对象时用 const 引用以避免拷贝;当函数需要修改对象时,使用非 const 引用。
#include <iostream>
#include <string>
#include <vector>
int square(int x) { return x * x; }
void addSuffix(std::string& s, const std::string& suffix = "!") {
s += suffix;
}
int main() {
int score = 72;
if (score >= 90) std::cout << "A\n";
else if (score >= 70) std::cout << "B\n"; // B
else std::cout << "C\n";
std::vector<int> nums{1, 2, 3};
for (int n : nums) std::cout << square(n) << " "; // 1 4 9
std::cout << "\n";
std::string word = "hi";
addSuffix(word);
std::cout << word << "\n"; // hi!
}类
类把数据成员和成员函数组织在一起。构造函数负责初始化对象;private 成员对外部代码隐藏。struct 与类基本相同,只是成员默认是 public。
#include <iostream>
#include <string>
#include <utility>
class Account {
public:
explicit Account(std::string owner) : owner_(std::move(owner)) {}
void deposit(double amount) {
if (amount > 0) balance_ += amount;
}
double balance() const { return balance_; }
const std::string& owner() const { return owner_; }
private:
std::string owner_;
double balance_ = 0.0;
};
int main() {
Account acc{"Ann"};
acc.deposit(50);
acc.deposit(-5); // ignored
std::cout << acc.owner() << ": " << acc.balance() << "\n"; // Ann: 50
}继承(class Savings : public Account)和 virtual 函数提供运行时多态。给重写的函数标上 override,让编译器帮你检查。
标准库容器与算法
标准库(常称 STL)提供了容器和算法,它们通过迭代器协同工作。
| 容器 | 类似 | 用途 |
|---|---|---|
std::vector<T> | 动态数组 | 默认的序列容器 |
std::array<T, N> | 固定长度数组 | 大小在编译期已知 |
std::map<K, V> | 有序字典 | 键有序,查找为 O(log n) |
std::unordered_map<K, V> | 哈希表 | 平均查找速度快,为 O(1) |
std::set<T> | 有序的不重复集合 | 判断成员是否存在,有序输出 |
#include <algorithm>
#include <iostream>
#include <map>
#include <string>
#include <vector>
int main() {
std::vector<int> nums{3, 1, 2, 5};
std::sort(nums.begin(), nums.end());
auto evens = std::count_if(nums.begin(), nums.end(),
[](int n) { return n % 2 == 0; });
std::cout << nums.front() << " " << nums.back() << " " << evens << "\n"; // 1 5 1
std::map<std::string, int> ages{{"bob", 25}, {"ann", 31}};
ages["cy"] = 40;
for (const auto& [name, age] : ages) { // structured bindings
std::cout << name << "=" << age << " "; // ann=31 bob=25 cy=40
}
std::cout << "\n";
}[](int n) { ... } 是 lambda,即就地书写的匿名函数。模板使同一个 std::vector 能适用于任何元素类型;你也可以用 template <typename T> 编写自己的模板。
标志性特性:RAII 与智能指针
RAII(Resource Acquisition Is Initialization,资源获取即初始化)指资源由某个对象持有:构造函数获取资源,析构函数释放资源。因为对象离开作用域时析构函数会自动运行,即使抛出了异常也一样,所以内存、文件和锁都不会泄漏。智能指针就是把 RAII 应用到堆内存上。
#include <iostream>
#include <memory>
struct Noisy {
int id;
explicit Noisy(int i) : id(i) { std::cout << "create " << id << "\n"; }
~Noisy() { std::cout << "destroy " << id << "\n"; }
};
int main() {
auto a = std::make_unique<Noisy>(1); // sole owner
{
auto b = std::make_shared<Noisy>(2); // shared, reference-counted
auto c = b; // two owners now
std::cout << "owners: " << b.use_count() << "\n";
} // destroy 2 happens here
std::cout << "end of main\n";
} // destroy 1 happens here输出:create 1、create 2、owners: 2、destroy 2、end of main、destroy 1。在现代代码中,你几乎不应该自己写 new 和 delete。
用异常处理错误
抛出异常对象,并以 const 引用捕获。标准异常类型位于 <stdexcept>。永远不会抛出异常的函数可以标记为 noexcept。
#include <iostream>
#include <stdexcept>
#include <string>
int parseAge(const std::string& text) {
int age = std::stoi(text); // throws std::invalid_argument on "abc"
if (age < 0) throw std::out_of_range("negative age");
return age;
}
int main() {
try {
std::cout << parseAge("41") << "\n"; // 41
std::cout << parseAge("abc") << "\n";
} catch (const std::invalid_argument&) {
std::cout << "not a number\n"; // not a number
} catch (const std::exception& e) {
std::cout << "error: " << e.what() << "\n";
}
}对于可预期的失败,许多代码库会返回 std::optional<T>(C++17)或 std::expected<T, E>(C++23),而不是抛出异常。
一个完整的小程序:词频统计
#include <algorithm>
#include <cctype>
#include <fstream>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>
int main(int argc, char* argv[]) {
if (argc != 2) {
std::cerr << "usage: " << argv[0] << " FILE\n";
return 1;
}
std::ifstream in(argv[1]);
if (!in) {
std::cerr << "cannot open " << argv[1] << "\n";
return 1;
}
std::unordered_map<std::string, int> counts;
std::string word;
while (in >> word) {
std::string clean;
for (unsigned char ch : word)
if (std::isalpha(ch)) clean += static_cast<char>(std::tolower(ch));
if (!clean.empty()) ++counts[clean];
}
std::vector<std::pair<std::string, int>> top(counts.begin(), counts.end());
std::sort(top.begin(), top.end(),
[](const auto& a, const auto& b) { return a.second > b.second; });
for (size_t i = 0; i < top.size() && i < 5; ++i)
std::cout << top[i].second << " " << top[i].first << "\n";
}用 g++ -std=c++20 -O2 words.cpp -o words 编译,再运行 ./words notes.txt。in 离开作用域时文件会自动关闭,这又是 RAII。
新手常见错误
- 在头文件中写
using namespace std;,这会污染所有包含它的文件。 - 本可以用
const&的地方,却按值传递std::vector之类的大对象。 - 在比较中混用有符号和无符号数,例如
int i与v.size()。开启警告就能看到这类问题。 - 在
push_back之后继续持有指向std::vector的迭代器或引用,元素可能已被移动。 - 使用原始的
new和delete,而不是std::make_unique、std::vector或其他拥有资源的类型。
接下来学什么
把 cppreference.com 当作语言和库的参考,并阅读 C++ Core Guidelines(由 Bjarne Stroustrup 和 Herb Sutter 编辑)了解良好实践。Standard C++ Foundation 的网站 isocpp.org 有一个入门页面,推荐了相关书籍。学习用 CMake 构建项目,然后继续学习移动语义、模板、constexpr,以及使用 std::thread 的并发编程。
常见问题
学 C++ 之前要先学 C 吗?
不是必须的。现代 C++ 教学从 std::string、std::vector 和类开始,之后才引入原始指针和 C 风格数组。不过先学 C 确实有助于理解内存。
应该使用哪个 C++ 标准?
当前的 GCC、Clang 和 MSVC 对 C++17 和 C++20 支持良好,对 C++23 的支持也在增加。请用 -std=c++20 这样的选项显式指定。
指针和引用有什么区别?
引用是一个别名,必须在创建时绑定,之后不能重新绑定,也不能为空。指针存储地址,可以为空,也可以改为指向别处。
C++ 难学吗?
这门语言很庞大,所以学习曲线比 Python 或 Go 更长。坚持使用现代特性、标准容器和智能指针,可以避开大部分经典陷阱。