C++ 基本内容
C++ 进程内存结构
进程即执行中的程序,包含 代码区与常量区
栈区
堆区
静态变量区
代码区与常量区(只读区)
存放代码本身和常量,这里的常量就是立即量,不是那种带 const 的变量
数字直接和代码存一块,而字符串存在常量区
栈区
函数执行用的内存,在函数结束后相应地销毁
堆区
用来分配的内存,需要手动销毁
-
堆区特点是大和灵活,存放大的对象并用指针管理
-
而程序的各种逻辑跑在栈中,如果栈可以只操作指针而不是把对象本体搬来搬去,可以显著减少负担
-
栈和堆的关系:堆用来存对象,栈进行对象的管理
静态变量区(可读写区)
存放静态和全局变量,静态和全局都能长期留在内存中,在编译时就创建了
- 区别仅为声明位置不同,导致静态作用范围比全局小,而且操作过程依然保持封装性
C++ 动态内存分配
单个分配
pointer = new SomeType
或 pointer = new SomeType(param)
进行省略空括号的默认构造,或带参构造(比如经常使用的拷贝构造)
多个分配 [ ]
pointer = new SomeType[n]
或 pointer = new SomeType[n]()
注意:基本类型的分配,带空括号初始化为 0,不带括号则为随机值
总结:一般建议不省略空括号
释放
delete pointer
/ delete[] pointer
避免内存泄漏:及时delete,或使用智能指针 / 异常处理
const 关键字
const
变量在编译期视为常量,但其本质,包括在进程内存的位置以及汇编层面体现都是变量
const 默认修饰的是左边的标识,如果左边没有才是修饰右边的标识,只要能修饰那就可以有多个 const
例1:
const int* const pi
第一个 const 表示值固定,第二个 const 表示指针固定(等同引用)例2:
int const*
和const int*
是一回事
auto 关键字
auto
关键字的意义,在于:
- 一定程度上减少了程序员检查类型的工作,在能确定类型的安全前提下,所以不要滥用
- 同理,在赋值时可以让编译器直接确定类型,不用检查左值类型的正确性,反而高效一点
- 在面向对象和模板代码中发光发热(比如名字很长但是很确定的类型,如迭代器),而简单的代码就比较随意
auto 的注意点
-
auto
不能推断引用,要加&
,而且推断引用实际上是推断引用指向的对象 -
auto
推断带const
的目标时如果是
auto
,则不保留const
如果是
auto&
,则会带上const
,变为 const 引用普通变量情况
指向常整数的指针常量,类型是指针,不加
&
时忽略了指针的 const,但保留指向对象的 const- 从安全角度出发,非引用推断只保留数值;引用推断与目标本身等价,所以也必须 const
- 由于指针是变量,不涉及
&
相关特性,于是在 auto 的使用中指针和引用就有了区别
-
当然如果确定为常量的话,直接手动
const auto
,这样既省心又直观
boost 库
更好的类型推断
#include
using boost::typeindex::type_id_with_cvr; auto a = 100; std::cout << type_id_with_cvr ().pretty_name() << std::endl; // 输出: int 万能引用、完美转发等
下载压缩包,解压,运行目录下脚本生成 b2.exe
然后搜索 VS2022 Native Tools 命令行,执行编译命令
其中指定工具集版本,项目右键
- 属性
- 常规
- 平台工具集
静态变量,指针和引用
关于静态变量,有一个说法:在编译时就为其分配了内存,早于程序的运行
没错,但说法有一定迷惑性,因为静态变量区本就是是进程的一部分
所以这里的提前分配和早于程序,可以认为是:一旦进程内存确定,静态变量内存就确定,提前分配的是相对空间
所以程序如果只编译的话,就是保留静态变量的分配方案。等什么时候运行,才进入内存
而代码执行过程中,再遇到静态变量的声明行,就会直接跳过了
关于指针,指针涉及的动作无外乎这几个:
- 取地址时对变量使用
&
- 按地址取值时对自己使用
*
- 通过加减,来按类型移动相应的地址
关于引用:除了一些特性外,
int& ref = i
等效int* const ptr = &i
左值和右值
C++任何一个对象要么是左值,要么是右值
左和右的说法是 C 语言的历史遗留,右值一定在
=
右边,但左值也可以在=
右边
左值:拥有地址属性的对象(比如一个变量,我们可以对它赋值,也就是把值存储到它的地址处,所以有地址属性)
右值:不满足左值条件的对象,也是不能放在等号左边的对象。临时对象就是右值
int i = 10; // 右值
int i1 = i; // 左值
int i2 = i + 1; // 右值
++i; // 左值, 因为i已经是自增后的值, 所以是直接取i自身地址
i++; // 右值, 因为取的是i自增前的值, 实际上是临时对象
引用的分类
提前看一眼,后面边学边涉及
-
普通左值引用:就是一个对象的别名,只能绑定左值,无法绑定常量对象
(如果不绑定一个确定的变量,那依然会有指针的内存不可控问题)
int i = 10; int& ri = i;
-
const左值引用:可以对常量起别名,可以绑定左值和右值
(这里没有不可控的危险了,所以可以当成常变量名)
const int& ri = 100;
-
右值引用:只能绑定右值的引用(涉及的概念已经脱离了传统指针,看后面内容)
int i = 20; int&& rri = i++;
-
万能引用:很重要,但需要模板等基础的概念,后面再说
move 函数
- 右值看重对象的值而不考虑地址,move函数可以对一个左值使用,使操作系统不再在意其地址属性,将其完全视作一个右值
int i = 50;
int&& rri = std::move(i);
-
move函数让操作的对象失去了地址属性
所以我们有义务保证以后不再使用该变量的地址属性,简单来说就是不再使用该变量,因为左值对象的地址是其使用时无法绕过的属性
相当于变量把自己的地址交了出来,仅带着值寻找下一个落脚点,至于有什么意义,后面会讨论
临时对象
右值引用主要处理的就是临时对象。所有临时对象都是右值,因为产生后很快就可能销毁,使用的是它的值属性
程序执行时生成的中间对象就是临时对象,如函数返回值、表达式等
可调用对象
函数
除了一般的函数外,C++ 提供了通过指针调用函数的功能
返回值(*)(参数)
定义了某个函数指针类型
返回值(*指针名)(参数)
声明一个该类的指针变量,指针名必须写在中间,作为左值很迷惑
- 但 C++11 引入了
using
语法糖来解决这个问题 ↓(using
是typedef
的上位替代)
using pf = void(*)(int); // pf可以完全替代右边的类型来声明变量
void t(int i){}
int main(){
int i = 50;
pf ptrT = t; // 有人说函数名本身就是指针,是错误的。这里其实是隐式执行 pf ptrT = &t
ptrT(i);
}
- 冷知识:关于
typedef
为何被替代的问题,简单解释一下:
typedef 原名 新名
,本该如此,但实际上该语法遵从的是声明变量的语法。何来此言?假设要为函数指针
void(*)(int, int)
起别名func_t
,代码是typedef void(*func_t)(int, int)
——给大伙整沉默了
仿函数
对类实现 ( )
运算符,( )
又称为函数调用运算符,可以包含参数,参数形式与函数相同
实现之后,可以对该类对象使用 (参数)
表现为调用(但目前还不知道有什么用)
class Test{
public:
int operator()(int t){
return t + 1;
}
};
Test test;
test(1); // 结果为2的右值
- 冷知识:类中定义了运算符,可以在对象后直接使用运算符,但也能直接调用运算符函数
class Test{ public: int operator+(int t) { return t + 2; } }; Test test; // 下面两个等价 cout << test + 2; cout << test.operator+(2);
lambda 表达式
[捕获列表](参数) -> 返回值 {函数体}
捕获
用捕获列表指定该 lambda 表达式可以访问的前文变量(因为 lambda 对象不遵从传统的作用范围,要手动指定)
[]
不捕获任何变量
[i]
捕获变量 i
(在函数中仅取其值)
[&i]
以引用捕获变量 i
(在函数中使用 i 的地址)
[=]
取值捕获所有变量
[&]
取引用捕获所有变量
可以混用捕获,例如:
[=, &i]
表示变量i
用引用传递,此外所有变量用值传递
[&, i]
表示变量i
用值传递,此外所有变量用引用传递
参数
形式与函数相同
返回值
可以正常设定类型,或指定为
auto
(auto
也可以推断void
),或直接省略,编译器依然会自动判断返回值