前言
现代C++语言可以看作是三部分组成的:
· 低级语言,大部分继承自C语言。
· 现代高级语言特性,允许我们定义自己的类型以及组织大规模程序和系统。
· 标准库,它利用高级特性来提供有用的数据结构和算法。
第1章 开始
1.1 编写一个简单的C++程序
int main ()
{
return 0;
}
每个C++程序都必须有一个main函数, main函数的返回值类型必须为int型。函数的定义包含四部分:返回类型(return type)、函数名(function name)、一个括号包围的形参列表(parameter list,允许为空)以及函数体(function body)。注意,大多数C++语句都要以分号结束。
1.2 初识输入输出
C++语言并未定义任何输入输出语句,取而代之,包含了一个全面的标准库来提供IO机制。
① iostream库
包含istream和ostream,分别表示输入流和输出流。一个流就是一个字符序列,是从IO设备读出或写入IO设备的。术语“流”(stream)想要表达的是,随着时间的推移,字符是顺序生成或消耗的。
② 标准输入输出对象
输入:标准输入(cin)
输出:标准输出(cout)、标准错误(cerr,输出警告和错误信息)、输出程序运行一般性信息(clog)
// 一般系统头文件用尖括号表示,用户自己编写的则用双引号。
// 一般将所有的#include指令都放在源文件开头位置,告诉编译器我们想要使用iostream库
#include <iostream>
int main()
{
// 标准输出,把右边的数据给cout对象,用 << 符号,endl表示结束当前行,并将与设备关联的缓冲区的内容刷到设备中
// 如果没有std::endl,并且在后面加一个while死循环,会发现屏幕一直不显示
// "Enter two numbers: "是字符串字面值常量
// std::endl是操纵符,写入endl的效果是结束当前行,并将与设备关联的缓冲区中的内容刷到设备中
std::cout << "Enter two numbers: " << std::endl;
int v1= 0, v2 = 0;
// 标准输入,把左边输入的数据给后面的变量,所以用 >> 符号
std::cin >> v1 >> v2;
std::cout << "The sum of " << v1 << " and " << v2
<< " is " << v1 + v2 << std::endl;
return 0;
}
1.3 注释简介
① 单行注释:以双斜线 // 开始,后面加一个空格,以换行符结束,常用于半行和单行注释。
② 界定符注释:以 /* 开始,以 */ 结束,通常用于多行注释。为了美观和规范,可以每行以星号空格开始写注释,从第二行开始书写。注意:界定符注释不能嵌套使用。
/*
* 功能:注释示例
* 作者:今天也要加油鸭!
* 版本号:v1.0
* 最后更新时间:2021/12/14
*/
int main ()
{
return 0;
}
个人补充: Doxygen 标准格式的 C++ 注释规范(供参考)
1. 文件头注释
/**
* @file 文件名.cpp
* @brief 文件功能摘要
* @details 详细说明(如核心逻辑、设计思想)
* @author 作者
* @date YYYY-MM-DD
* @version 版本号
* @par 版权声明
* Copyright (c) 2025, 公司名
*/
标签说明:
@file
后建议不写文件名,Doxygen 会自动识别
2. 函数注释
/**
* @brief 计算两数之和
* @details 接收两个整数,返回它们的和
* 超出整数范围时行为未定义
*
* @param num1 第一个整数(输入参数)
* @param num2 第二个整数(输入参数)
* @return int 两数之和
* @note 参数必须为整数类型
* @warning 不检查溢出
* @see add_vector() 相关函数
*/
int add(int num1, int num2);
标签说明:
@param [in]/[out]
:标注参数方向(输入/输出)。@retval
:具体返回值意义(如错误码)。@note
:注意事项,@warning
:警告信息
3. 类与结构体注释
/// @brief 错误信息打印器(单例模式)
/// @details 全局错误日志管理类,支持多线程安全。
class ErrorLogger {
public:
int level; ///< 日志级别(0-关闭,1-开启)
/**
* @brief 打印错误消息
* @param msg 待打印的错误信息
*/
void log(const std::string& msg);
};
成员变量:
- 行尾注释用
///<
- 行上注释用
///
4. 枚举与宏注释
/// @brief 颜色枚举定义
enum Color {
RED, ///< 红色标识
BLUE ///< 蓝色标识
};
/** @brief 缓冲区大小 */
#define BUFFER_SIZE 1024
5. 高级标签扩展
标签 | 用途 | 示例 |
@todo |
待实现功能 | @todo 支持浮点数运算 |
@deprecated |
标记已废弃 | @deprecated 使用 add_v2() |
@code |
嵌入示例代码 | @code{.cpp} ... @endcode |
@sa |
参见其他文档条目 | @sa ErrorLogger::level |
1.4 控制流
① while语句:反复执行一段代码,直至给定条件为假为止
while (condition)
statement
② for语句:while在循环条件中检测变量、在循环体中递增变量的模式使用非常频繁,for语句可以简化符合这种模式的语句。
每个for语句都包含两部分:循环头和循环体。循环头控制循环体的执行次数,它由三部分组成:一个初始化语句、一个循环条件以及一个表达式。
for(int val = 1; val <= 10; ++val)
sum += val;
③ 读取数量不定的输入数据:预先不知道要对多少个数求和,需不断读取数据直至没有新的输入为止。
while (std::cin >> value) //遇到文件结束符,或遇到一个无效的输入,条件变为假,终止输入,Windows中文件结束符为:Ctrl+Z。
sum += value;
当我们使用一个istream对象作为条件时,其效果是检测流的状态:如果流是有效的,即流未遇到错误,那么检测成功。当遇到文件结束符,或遇到一个无效输入时(例如读入的值不是一个整数),istream对象的状态会变为无效。处于无效状态的istream对象会使条件变为假。因此,我们的while循环会一直执行直至遇到文件结束符(或输入错误)。
④ if语句:条件执行
if (condition){
statement;
} else {
}
注:C++用=进行赋值,用==作为相等运算符,两个运算符都可以出现在条件中,一个常见的错误是想在条件中使用==(相等判断),却误用了=。
1.5 类简介
我们通过定义一个类(class)来定义自己的数据结构。一个类定义了一个类型,以及与其关联的一组操作。
关键概念:类定义了行为。
成员函数:成员函数是定义为类的一部分的函数,有时也被称为方法(method)。
① 点运算符(.):只能用于类类型的对象,其左侧运算对象必须是一个类型的对象,右侧运算对象必须是该类型的一个成员名,运算结果为右侧运算对象指定的成员。
② 调用运算符(()):用来调用一个函数,里面放置实参列表。
1.6 书店程序
//Sales_item.h
#ifndef SALESITEM_H
// 防止头文件重复包含的保护宏
#define SALESITEM_H
// 包含必要的标准库头文件
#include <iostream>
#include <string>
/**
* @class Sales_item
* @brief 销售项类,用于表示书籍销售数据
*
* @details 该类封装了书籍销售信息,包括:
* - 书籍 ISBN 编号
* - 销售数量
* - 销售总收入
* 支持基本的算术运算和流输入输出操作
*/
class Sales_item {
// 声明友元函数(运算符重载)
friend std::istream& operator>>(std::istream&, Sales_item&);
friend std::ostream& operator<<(std::ostream&, const Sales_item&);
friend bool operator<(const Sales_item&, const Sales_item&);
friend bool operator==(const Sales_item&, const Sales_item&);
public:
// ==================== 构造函数 ====================
/**
* @brief 默认构造函数
* @details 初始化销售数量为0,总收入为0.0
*/
Sales_item(): units_sold(0), revenue(0.0) { }
/**
* @brief 带ISBN参数的构造函数
* @param book 书籍的ISBN编号
*/
Sales_item(const std::string &book):
bookNo(book), units_sold(0), revenue(0.0) { }
/**
* @brief 从输入流构造对象
* @param is 输入流对象
*/
Sales_item(std::istream &is) { is >> *this; }
public:
// ==================== 成员函数 ====================
/**
* @brief 复合加法赋值运算符
* @param rhs 要添加的销售项
* @return 当前对象的引用
*/
Sales_item& operator+=(const Sales_item&);
/**
* @brief 获取书籍ISBN编号
* @return ISBN字符串
*/
std::string isbn() const { return bookNo; }
/**
* @brief 计算平均售价
* @return double 平均价格(销售数量为0时返回0)
*/
double avg_price() const;
private:
// ==================== 数据成员 ====================
std::string bookNo; ///< 书籍的唯一ISBN编号
unsigned units_sold; ///< 销售总数
double revenue; ///< 销售总收入
};
// ==================== 相关全局函数 ====================
/**
* @brief 比较两个销售项的ISBN编号
* @param lhs 左侧销售项
* @param rhs 右侧销售项
* @return bool ISBN是否相同
* @note 用于排序和比较操作
*/
inline bool compareIsbn(const Sales_item &lhs, const Sales_item &rhs)
{ return lhs.isbn() == rhs.isbn(); }
/**
* @brief 销售项加法运算符
* @param lhs 左侧销售项
* @param rhs 右侧销售项
* @return Sales_item 相加后的新销售项
*/
Sales_item operator+(const Sales_item&, const Sales_item&);
/**
* @brief 相等比较运算符
* @param lhs 左侧销售项
* @param rhs 右侧销售项
* @return bool 是否相等
*/
inline bool operator==(const Sales_item &lhs, const Sales_item &rhs)
{
return lhs.units_sold == rhs.units_sold &&
lhs.revenue == rhs.revenue &&
lhs.isbn() == rhs.isbn();
}
/**
* @brief 不等比较运算符
* @param lhs 左侧销售项
* @param rhs 右侧销售项
* @return bool 是否不等
*/
inline bool operator!=(const Sales_item &lhs, const Sales_item &rhs)
{
return !(lhs == rhs); // 复用相等运算符实现
}
// ==================== 成员函数实现 ====================
/**
* @brief 复合加法赋值运算符实现
* @details 将右侧销售项的数据累加到当前对象
* @warning 默认假设两个销售项的ISBN相同
*/
Sales_item& Sales_item::operator+=(const Sales_item& rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
/**
* @brief 加法运算符实现
* @details 创建左侧对象的副本并与右侧对象相加
* @return Sales_item 包含累加结果的新对象
*/
Sales_item operator+(const Sales_item& lhs, const Sales_item& rhs)
{
Sales_item ret(lhs); // 复制左侧对象
ret += rhs; // 累加右侧对象数据
return ret; // 返回结果对象
}
// ==================== 流运算符实现 ====================
/**
* @brief 输入流运算符
* @details 格式:ISBN 销售数量 单价
* @param in 输入流
* @param s 目标Sales_item对象
* @return std::istream& 输入流引用
*/
std::istream& operator>>(std::istream& in, Sales_item& s)
{
double price;
in >> s.bookNo >> s.units_sold >> price;
// 检查输入是否成功
if (in)
s.revenue = s.units_sold * price;
else
s = Sales_item(); // 输入失败则重置对象
return in;
}
/**
* @brief 输出流运算符
* @details 格式:ISBN 销售数量 总收入 平均价格
* @param out 输出流
* @param s 要输出的Sales_item对象
* @return std::ostream& 输出流引用
*/
std::ostream& operator<<(std::ostream& out, const Sales_item& s)
{
out << s.isbn() << " " << s.units_sold << " "
<< s.revenue << " " << s.avg_price();
return out;
}
/**
* @brief 计算平均售价实现
* @return double 平均价格
*/
double Sales_item::avg_price() const
{
// 避免除零错误
return units_sold ? revenue/units_sold : 0.0;
}
#endif // SALESITEM_H
/**
* @file book_sales.cpp
* @brief 书籍销售统计程序
* @details 该程序读取一系列书籍销售记录,按ISBN编号分组并统计每组的销售总额
*/
#include <iostream> // 标准输入输出流库
#include "Sales_item.h" // 自定义的销售项类头文件
/**
* @brief 主函数
* @return int 程序执行状态码(0表示成功)
*
* @details 程序处理流程:
* 1. 读取第一条销售记录作为当前分组
* 2. 逐条读取后续记录:
* - 若ISBN与当前分组相同,则累加销售数据
* - 若ISBN不同,则输出当前分组并开始新分组
* 3. 处理结束后输出最后一组统计结果
*
* @note 输入格式要求:
* ISBN编号 销售数量 单价
* 例如:0-201-70353-X 4 24.99
*
* @warning 所有销售记录必须按ISBN排序输入
*/
int main()
{
Sales_item total; // 当前分组的总销售项
// 尝试读取第一条销售记录
if (std::cin >> total) {
Sales_item trans; // 暂存当前读取的销售项
// 循环读取后续销售记录
while (std::cin >> trans) {
// 检查ISBN是否与当前分组相同
if (total.isbn() == trans.isbn()) {
total += trans; // 相同ISBN,累加到当前分组
} else {
// 输出当前分组统计结果
std::cout << total << std::endl;
total = trans; // 开始处理新ISBN分组
}
}
// 输出最后一组统计结果
std::cout << total << std::endl;
} else {
// 处理无输入数据的情况
std::cerr << "No data?!" << std::endl;
return -1; // 返回错误状态码
}
return 0; // 正常退出
}
1.7 其他
① 除非特别需要,尽量使用 ++i 而不是 i++。
② C++ Primer 英文版官方勘误:C++ Primer errata
③ 术语表
缓冲区(buffer)一个存储区域,用于保存数据。IO设施通常将输入(或输出)数据保存在一个缓冲区中,读写缓冲区的动作与程序中的动作是无关的。我们可以显式地刷新输出缓冲,以便强制将缓冲区中的数据写入输出设备。默认情况下,读cin会刷新cout;程序非正常终止时也会刷新cout。
Cerr 一个ostream对象,关联到标准错误,通常写入到与标准输出相同的设备。默认情况下,写到cerr的数据是不缓冲的。cerr通常用于输出错误信息或其他不属于程序正常逻辑的输出内容。
clog 一个ostream对象,关联到标准错误。默认情况下,写到clog的数据是被缓冲的。clog通常用于报告程序的执行信息,存入一个日志文件中。
文件结束符(end-of-file) 系统特定的标识,指出文件中无更多数据了。
库类型(library type)标准库定义的类型,如istream。
操纵符(manipulator)对象,如std::endl,在读写流的时候用来“操纵”流本身。
命名空间(namespace)将库定义的名字放在一个单一位置的机制。命名空间可以帮助避免不经意的名字冲突。C++标准库定义的名字在命名空间std中。
未初始化的变量(uninitialized variable)未赋予初值的变量。类类型的变量如果未指定初值,则按类定义指定的方式进行初始化。定义在函数内部的内置类型变量默认是不初始化的,除非有显式的初始化语句。试图使用一个未初始化变量的值是错误的。未初始化变量是bug的常见成因。
.运算符(.operator)点运算符,左侧运算对象必须是一个类类型对象,右侧运算对象必须是此对象的一个成员的名字,运算结果即为该对象的这个成员。
参考教程:C++概述
第 2 章 变量和基本类型
2.1 基本内置类型
C++定义了一套包括算术类型和空类型(void)在内的基本数据类型。其中算术类型包含了字符、整型数、布尔值和浮点数。空类型不对应具体的值,仅用于一些特殊的场合,例如最常见的是,当函数不返回任何值时使用空类型作为返回类型。
2.1.1 算术类型
算术类型分为两类:整型(integral type,包括字符和布尔类型在内)和浮点型。
类型分类 | 具体类型 | 最小尺寸要求 | 典型尺寸 | 取值范围(典型) | 用途说明 |
---|---|---|---|---|---|
布尔型 | bool |
未指定 | 1字节 | true 或 false |
逻辑判断 |
字符型 | char |
至少 8 位 | 1字节 | -128 到 127 或 0 到 255 | 字符存储 |
signed char |
至少 8 位 | 1字节 | -128 到 127 | 有符号字符 | |
unsigned char |
至少 8 位 | 1字节 | 0 到 255 | 字节操作 | |
wchar_t |
至少 16 位 | 2/4字节 | 0 到 65,535 或 更大 | 宽字符存储 | |
(C++11) |
至少 16 位 | 2字节 | 0 到 65,535 | UTF-16 字符 | |
(C++11) |
至少 32 位 | 4字节 | 0 到 4,294,967,295 | UTF-32 字符 | |
整型 (有符号) |
( |
至少 16 位 | 2字节 | -32,768 到 32,767 | 小整数 |
int |
至少 16 位 | 4字节 | -2,147,483,648 到 2,147,483,647 | 通用整数 | |
( |
至少 32 位 | 4/8字节 | -2,147,483,648 到 2,147,483,647 | 大整数 | |
(C++11) |
至少 64 位 | 8字节 | -9.2e18 到 9.2e18 | 超大整数 | |
整型 (无符号) |
unsigned short |
至少 16 位 | 2字节 | 0 到 65,535 | 小正整数 |
unsigned int |
至少 16 位 | 4字节 | 0 到 4,294,967,295 | 通用正整数 | |
unsigned long |
至少 32 位 | 4/8字节 | 0 到 4,294,967,295 或更大 | 大正整数 | |
unsigned long long |
至少 64 位 | 8字节 | 0 到 18.4e18 | 超大正整数 | |
浮点型 | float |
6位小数精度 | 4字节 | ±3.4e-38 到 ±3.4e38 (7位精度) | 单精度浮点 |
double |
10位小数精度 | 8字节 | ±1.7e-308 到 ±1.7e308 (15位精度) | 双精度浮点 | |
long double |
10位小数精度 | 8/16字节 | ±1.1e-4932 到 ±1.1e4932 (19+位精度) | 扩展精度 | |
特殊类型 | size_t |
平台相关 | 4/8字节 | 同 unsigned long 或 unsigned long long |
大小表示 |
ptrdiff_t |
平台相关 | 4/8字节 | 同 long 或 long long |
指针差值 |
字面值后缀示例:
42 // int
42u // unsigned int
42l // long
42ll // long long
3.14f // float
3.14 // double
3.14L // long double
'x' // char
u'猫' // char16_t
U'🐱' // char32_t
L'字' // wchar_t
类型查询示例:
#include <iostream>
#include <type_traits>
#include <cstdint>
int main() {
std::cout << "int size: " << sizeof(int) << " bytes\n";
std::cout << "double precision: " << std::numeric_limits<double>::digits10 << " digits\n";
std::cout << "char is signed: " << std::is_signed<char>::value << "\n";
// 固定宽度类型(C++11)
int32_t fixed; // 精确32位有符号整数
}
建议:① 当明确知晓数值不可能为负,建议选用无符号类型。
② 整数运算用 int,超过 int 表示范围时用 long long。
③ 算术表达式中不要使用char或bool,只有在存放字符或布尔值时才使用它们。因为类型char在一些机器上是有符号的,而在另一些机器上又是无符号的,所以如果使用char进行运算特别容易出问题。如果你需要使用一个不大的整数,那么明确指定它的类型是signed char或unsigned char。
④ 浮点数运算用 double 即可,因为float通常精度不够而且双精度浮点数和单精度浮点数的计算代价相差无几。
类型选择建议:
- 整数:优先用
int
或size_t
(表示大小) - 浮点:优先用
double
(精度和性能平衡) - 避免默认
char
的有符号性依赖 - 明确大小要求时使用
<cstdint>
中的固定宽度类型(如int32_t
)
2.1.2 类型转换
Ⅰ)自动类型转换
1)将一种类型的数据赋值给另外一种类型的变量时就会发生自动类型转换。如:
float f = 10; // 10是int类型,先转换为float类型10.000000,再赋给f
int m = f; // f是float类型,先转换为int类型10,再赋给m
int n = 3.14; // 3.14是float类型,先转换为int类型3,再赋给n
2)在不同类型的混合运算中,编译器也会自动地进行数据类型转换,将参与运算的所有数据先转换为同一种类型,然后再进行计算。转换规则如图:
注:如上图所示,当一个算术表达式中既有无符号数又有 int 值时,那么 int 值会转换成无符号数。因此,切勿混用带符号类型和无符号类型。
unsigned u = 10;
int i = -42;
std::cout << i+i << std::endl; // 输出-84
std::cout << u+i << std::endl; // 如果int占32位,输出4294967264
Ⅱ) 强制类型转换
格式:(type_name)expression 例如:
(float) i;
(int) (a+b);
(float) 10;
(double) x/y; // 先将 x 转换为 double 类型,再除以 y。
(double) (x/y); // 先计算 x/y,再转换为 double 类型。对于除法运算,若 x 与 y 都是整数,则运算结果也是整数,小数部分将被直接丢弃;若 x 与 y 有一个是小数,则运算结果也是小数。
注意:自动类型转换和强制类型转换,都只是本次运算临时性的,不会改变数据本来的类型或者值。
2.1.3 字面值常量
主要包括:整型和浮点型、字符和字符串、转义序列、布尔和指针等字面值常量,同时,还可以指定字面值类型。
字符和字符串字面值 |
|||
前缀 |
含义 |
类型 |
|
u |
Unicode 16字符 |
char16_t |
|
U |
Unicode 32字符 |
char32_t |
|
L |
宽字符 |
wchar_t |
|
u8 |
UTF-8 |
char |
|
整型字面值 |
浮点型字面值 |
||
后缀 |
最小匹配类型 |
后缀 |
类型 |
u or U |
unsigned |
f或F |
float |
l or L |
long |
l或L |
long double |
ll or LL |
long long |
2.2 变量
变量提供一个具名的、可供程序操作的存储空间。C++中的每个变量都有其数据类型,数据类型决定着变量所占内存空间的大小和布局方式、该空间能存储的值的范围,以及变量能参与的运算。对C++程序员来说,“变量(variable)”和“对象(object)”一般可以互换使用。
2.2.1 变量定义
Ⅰ) 变量定义基本形式: 类型说明符 变量名1,变量名2,...;
在C++语言中,初始化和复赋值是两个完全不同的概念。初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来替代。
注意初始化和赋值的区别,常见的初始化形式有:
int a = 0;
int a = {0};
int a{0};
int a(0);
其中,通过花括号进行初始化的形式被称为列表初始化。 如果使用列表初始化且初始值存在丢失信息的危险,编译器将报错。
#include <iostream>
void function(){
int i;
std::cout << "普通函数内未初始化的值:" << i << std::endl;
}
int k;
int main(int argc, char const *argv[])
{
function();
int j;
std::cout << "main函数内未初始化的值:" << j << std::endl;
std::cout << "全局未初始化的值:" << k << std::endl;
return 0;
}
如果定义变量时没有指定初值,则变量被默认初始化,此时变量被赋予了“默认值”。对于未初始化的变量,输出如下:
普通函数内未初始化的值:22007
main函数内未初始化的值:1525802986
全局未初始化的值:0
定义于函数体内的内置类型的对象如果没有初始化,则其值未定义。类的对象如果没有显式地初始化,则其值由类确定。
2.2.2 变量声明和定义的关系
为了支持分离式编译,C++语言将声明和定义区分开来。声明(declaration)使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义(definition)负责创建与名字关联的实体。
声明 规定了变量的类型和名字,使得名字为程序所知。
定义 负责创建与名字相关的实体,申请内存空间,也可能为变量赋一个值。
变量只能被定义一次,但是可以被多次声明。在C++项目开发中,声明一般统一放在头文件里面,在源程序中定义,并且建议在第一次使用变量时再定义。
如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显式地初始化变量。任何包含了显式初始化的声明即成为定义。我们能给由extern关键字标记的变量赋一个初始值,但是这么做也就抵消了extern的作用。extern语句如果包含初始值就不再是声明,而变成定义了。
extern int i; // 想要声明 i,而非定义 i
int j; // 定义 j
extern double pi = 3.1416; // 因为有了显式初始化,extern 的作用被抵消,这里变成定义了,注意:只能作为全局变量的定义,即放在 main 函数外面。
静态类型:C++是一种静态类型语言,其含义是在编译阶段检查类型。其中,检查类型的过程称为类型检查。对象的类型决定了对象所能参与的运算,在C++语言中,编译器负责检查数据类型是否支持要执行的运算,如果试图执行类型不支持的运算,编译器将报错并且不会生成可执行文件。程序越复杂,静态类型检查越有助于发现问题。然而,前提是编译器必须知道每一个实体对象的类型,这就要求我们在使用某个变量之前必须声明其类型。
2.2.3 标识符
C++的标识符(identifier)由字母、数字和下画线组成,其中必须以字母或下画线开头。标识符的长度没有限制,但是对大小写字母敏感。
2.2.4 名字的作用域
作用域(scope)是程序的一部分,在其中名字有其特定的含义。C++语言中大多数作用域都以花括号分隔。同一个名字在不同的作用域中可能指向不同的实体。名字的有效区域始于名字的声明语句,以声明语句所在的作用域末端为结束。
如果函数有可能用到某全局变量,则不宜再定义一个同名的局部变量。
2.3 复合类型
复合类型(compound type)是指基于其他类型定义的类型。C++语言有几种复合类型,本章将介绍其中的两种:引用和指针。
2.3.1 引用
C++11中新增了一种引用:所谓的“右值引用(rvalue reference)”,我们将在13.6.1节(第471页)做更详细的介绍。这种引用主要用于内置类。严格来说,当我们使用术语“引用(reference)”时,指的其实是“左值引用(lvalue reference)”。
引用:就是为对象起了另外一个名字(引用即别名),定义引用时就把引用和它的初始值绑定了,并且不能重新绑定,因此引用必须初始化,且初始值必须是一个对象而不是字面值。
int a;
int &b = a; // b其实只是a的一个别名,注意b不是对象。
int &c; // 报错!引用必须初始化
为引用赋值,实际上是把值赋给了与引用绑定的对象;获取引用的值,实际上是获取了与引用绑定的对象的值;同理,以引用作为初始值,实际上是以与引用绑定的对象作为初始值。
注意:1. 因为引用本身不是一个对象,所以不能定义引用的引用。
2. 引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起。
2.3.2 指针
指针(pointer)是“指向(point to)”另外一种类型的复合类型。与引用类似,指针也实现了对其他对象的间接访问。然而指针与引用相比又有很多不同点。
1. 指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。
2. 指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
int a = 1;
int *p = &a;
int *p1 = nullptr; // 空指针
void *p2 = &a; // p2可以用来存放任意对象的地址,这里存放int类型的 a 的地址
std::cout << *p;
指针和定义的区别:指针本身是一个对象,允许赋值和拷贝,可以先后指向不同的对象,并且定义时不是必须初始化(不过建议初始化为 nullptr 或者 0)。空指针
不指向任何对象,在试图使用一个指针之前可以首先检查它是否为空。得到空指针最直接的办法就是用字面值nullptr来初始化指针,这也是C++11新标准刚刚引入的一种方法。nullptr是一种特殊类型的字面值,它可以被转换成任意其他的指针类型。也可以通过将指针初始化为字面值0来生成空指针,过去的程序还会用到一个名为NULL的预处理变量来给指针赋值,这个变量在头文件cstdlib中定义,它的值就是0。在新标准下,现在的C++程序最好使用nullptr,同时尽量避免使用NULL。
void* 指针
void*是一种特殊的指针类型,可用于存放任意对象的地址。一个void*指针存放着一个地址,这一点和其他指针类似。不同的是,我们对该地址中到底是个什么类型的对象并不了解。利用void*指针能做的事比较有限:拿它和别的指针比较、作为函数的输入或输出,或者赋给另外一个void*指针。不能直接操作void*指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。
2.3.3 理解复合类型的声明
指向指针的指针:当有多个修饰符连写在一起时,按照其逻辑关系详加解释即可。以指针为例,指针是内存中的对象,像其他对象一样也有自己的地址,因此允许把指针的地址再存放到另一个指针当中。
指向指针的引用:引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用。*&r 从右往左阅读,离变量名最近的符号对变量的类型有最直接的影响,所以上面的r是一个引用,*说明引用的是一个指针。因此,r是指向int类型指针的引用。
int i = 42;
int *p; // p是int型指针
int *&r = p; // r是对指针p的引用
r = &i;
*r = 0;
问题1:int *&r=p
和 int &*r=p
哪个正确?
-
int *&r = p
正确 ✅-
含义:
r
是一个引用(&
),引用对象是指向int
的指针(*
) -
要求:
p
必须是指向int
的指针类型(如int* p
) -
示例:
-
int x = 10;
int *p = &x; // p 是指向 int 的指针
int *&r = p; // r 是指针 p 的引用
*r = 20; // 通过引用修改 x 的值
-
int &*r = p
错误 ❌-
原因:不能定义指向引用的指针
-
引用只是别名,不是独立对象,没有内存地址
-
编译器会报错:
cannot declare pointer to 'int&'
-
问题2:如何快速可靠地理解复合类型声明?
核心技巧:从右向左阅读规则(结合符号优先级)
-
从变量名开始
-
向右解析数组/函数
-
向左解析指针/引用