【C++】C++ 杂谈

常量指针是一个指针,指向的对象是常量;指针常量是一个常量指针,不能修改指向对象;常量指针常量则是指向常量的常量指针,即不能修改指向对象,也不能通过指针修改对象的值。

const

const 作用

提高健壮性,起到保护作用。

  1. 声明常量,阻止其值被修改;
  2. 声明常量指针和指针常量,阻止指针或指针指向的对象被修改;
  3. 修饰形参,阻止函数修改参数;
  4. 声明成员函数为常函数,常函数无法修改成员变量的值;
  5. 限定函数返回值,使其返回值不能为“左值”。

左值:指向内存位置的表达式,即可以修改的值;
右值:指向存储在内存中某个地址的数值,即不能修改的值。

常量&指针

  • 常量指针 const int*
    顾名思义这是一个指针(变量),指向的对象是一个常量,不能使用常量指针来修改指向的对象的值,但可以修改指针的指向对象,因为指针是变量;
    常量指针指向的是一个常量,变量和常量都可以赋值给常量,因此常量指针指向的对象可以是常量也可以是变量,但即使指向的是变量,也不可以通过指针来修改指向对象的值;

  • 指针常量 int* const
    顾名思义这是一个常量,只不过它的类型是指针类型,因此不能改变指针的指向对象,但可以通过指针修改指向对象的值;
    数组名就是数组首地址的别名,即指向数组首地址的一个指针且不能修改指向,所以数组名其实就是一个指针常量;
    指针常量是一个常量,可以把一个常量或变量赋值给它,但必须在定义的时候赋值(常量的要求),而且定义之后不能再修改它的值,所以无法修改其指向的对象,但可以通过指针常量修改指向对象的值,因此其指向对象只能是变量不能是常量

  • 常量指针常量 const int* const
    指向常量的指针常量,常量指针+指针常量,既不可以修改指针指向的对象,也不可以通过指针来修改指向对象的值;
    首先它是一个常量,所以要在定义的时候赋值且不能再修改它的值(指向对象),其次它指向的是常量,所以也不能通过它修改指向对象的值,指向对象只能是常量不能是变量

记忆方法:看后缀名词,const int* 常量指针,后缀是指针,所以这是个指针变量,指向的对象是常量;int* const 指针常量,后缀是常量,所以这是个常指针,不能修改其指向,但指向的对象是个变量。

指针

指针数组

  • 指针数组 int* ptrs[3];
    顾名思义这是一个数组,数组的元素是指针,sizeof(ptrs) 也就是数组所占字节,即数组元素个数乘以指针变量所占字节数;
1
2
3
int* pt;
int* ptrs[3] = {pt};
std::cout << sizeof(ptrs) << std::endl; // 24
  • 数组指针 int arr[] = {}; int* pt = arr;
    顾名思义这是一个指针,指向数组首地址,sizeof(pt) 为指针变量所占字节数,与数组大小无关;另外数组名虽然也是指向数组首地址,但 sizeof(arr) 为数组实际所占字节。
1
2
3
4
5
6
int arr[] = {};
int* pt1 = arr;
std::cout << sizeof(arr) << " " << sizeof(pt1) << std::endl; // 0 8
int arr2[] = {0, 1, 2};
pt1 = arr2;
std::cout << sizeof(arr2) << " " << sizeof(pt1) << std::endl; // 12 8

指针&引用

  • 指针是一个变(常)量,只不过存储的是一个内存地址,指向内存的一个存储单元;引用是变量的一个别名而已,不需要额外的内存空间;
  • 指针可以有多级,引用只能有一级;
  • 指针可以为空,引用必须在定义的时候初始化;
  • 指针的值可以修改,可以随时指向其它存储单元(指针常量除外),引用的值初始化后就不能修改;
  • 作为参数传递时,指针是值传递,引用则是引用传递,对参数的任何操作都会通过一个间接寻址影响调用函数传进来的实参;
  • sizeof(引用) 得到的是原对象的大小,sizeof(指针) 得到的是指针本身的大小。

内存

内存布局

C++ 程序编译时内存分为 5 大存储区:

  • 栈区:由编译器分配和释放,存放函数的参数值,局部变量值等,操作方法类似于数据结构中的栈;
  • 堆区:由程序员分配和释放,未释放在程序退出时由操作系统释放,分配方式类似于数据结构中的链表;
  • 全局/静态区:由编译器分配和释放,存储程序的全局变量和静态变量;
    全局/静态变量放在同一个存储区,这个区分为两部分,初始化的变量放在数据区(data segment),未初始化的放在 bbs(block started by symbol),bbs 在编译好的文件中不被分配内存,只是记录所需大小;

    把初始化和未初始化的变量分为两个区存放的原因有两个:

    1. 内存是否分配的区别;
    2. 未初始化的变量放在 bbs 区,在程序启动时可以统一调用 memset 来初始化。
  • 文字常量区:存放常量字符串,程序退出后由操作系统释放;
  • 程序代码区:存放编译后的二进制代码。

内存分配

内存分配的三种方式:

  1. 从静态存储区分配,内存在程序编译的时候就已经分配好了,在程序运行的整个过程都存在,直接程序退出才由 OS 释放,主要存放全局变量、静态变量;
  2. 从栈上创建,执行函数时,函数的参数和局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放;栈内存分配运算内置于处理器的指令集中,效率很高,但分配的内存容量有限;
  3. 从堆上分配,也称为动态内存分配,在程序运行过程中由程序员手动调用 mallocnew 申请任意多少的内存,再由程序员负责在适当的时机调用 freedelete 释放内存;动态内存分配非常灵活,但需要程序员处理好每次分配回收,否则就会出现内存泄露,另外频繁的分配和回收不同大小的堆空间也容易产生内存碎片。

栈和堆的区别

  • 申请方式
    栈由系统自动分配和释放,堆需要程序员自己申请和释放,并指明大小。
  • 系统响应
    栈:只要栈的剩余空间大于所需空间,系统就为程序提供内存,否则将报异常提示栈溢出;
    堆:当收到分配内存的申请时系统先遍历一个记录空闲内存块地址的链表,找到第一个空间大于所申请空间的堆结点,然后把这个结点从空闲内存链表中删除,并将该结点的空间分配给程序,如果堆结点比申请空间大,则把多余的部分重新生成一个结点放入到空闲内存链表中,这也是内存碎片产生的原因。另外系统会在内存空间的首地址记录本次分配的大小,方便 delete 的时候正确释放这块内存空间。
  • 大小限制
    栈:栈的大小十分有限,Windows 系统一般是 1M 或 2M,它是一个编译时就确定的常数,如果申请空间超过栈的剩余空间,将提示 overflow;
    堆:堆的大小受限于计算机系统中有效的虚拟内存,一般都够用,除非计算机的内存不足了。
  • 生长方向
    栈:栈由高地址向低地址生长,是一块连续的内存区域,栈顶地址、栈底地址和栈最大容量都是系统预先规定好的;
    堆:堆由低地址向高地址生长,是一块不连续的内存区域,这是因为链表的遍历方向由低地址向高地址且不连续。
  • 申请效率
    栈由系统分配速度较快,堆由程序分配速度较慢。另外在 Windows 下最好的方式是使用 VirtualAlloc 分配内存,它既不是在堆也不是在栈而是在进程的地址空间中保留一块内存,速度快且灵活。
  • 存储内容
    栈:在函数调用时第一个进栈的函数调用后的下一条可执行语句的地址,然后是函数的各个参数(大多数编译器的参数是从右往左入栈的),然后是函数中的局部变量;
    堆:一般在堆的头部用一个字节存放堆的大小,往后的内容由程序员自行安排。

内存布局

动态内存分配方式

动态内存分配和释放有两种方式,malloc/freenew/delete,两种方式的区别:

  • new 可以看成是在 malloc 的基础上增加了构造函数的执行,new 出来的指针是直接带类型信息的,而 malloc 返回的都是 void*
  • 对于”非内部数据类型“的对象只能使用 new/delete 不能使用 malloc/free,因为对象在创建的同时要自动执行构造函数,销毁时要自动调用析构函数;
  • malloc/free 是 C 语言的标准库函数,new/delete 是 C++ 的运算符;
  • C 语言只有 malloc/free,C++ 才有 new/delete,C++ 可以调用 C 函数。

存储类

C++ 共有 auto register static extern mutable 5 种存储类。

auto

auto 是所有局部变量默认的存储类,auto 只能用在函数内且只能修饰局部变量。

1
2
3
4
5
{
// 这两条语句等价
int num1;
auto int num2;
}

register

register 用于定义存储在寄存器而非 RAM(内存)中的局部变量,这意味着变量的最大尺寸等于寄存器的大小,而且不能使用取地址运算符&,因为它没有内存地址。register 存储类只用于需要快速访问的变量,另外把变量定义成 register 并不意味着它就一定被存储在寄存器中,只是可能存储在寄存器中。

1
2
3
{
register int count;
}

static

static 修饰局部变量,提示编译器局部变量的生命周期为整个程序运行过程,每次进入和离开作用域时不用进行创建和销毁,局部变量在函数调用时保持原来的值;
static 修改全局变量,限制其作用域在声明它的文件内;
static 修改类成员,表示成员的副本被类的所有对象共享。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static int num = 10; // 静态全局变量

void func()
{
static int i = 0; // 静态局部变量
std::cout << i++ << std::endl;
}

class Foo
{
public:
static int idx; // 静态成员属性
static void test(); // 静态成员函数
}

extern

extern 用于提供全局变量的引用,对于无法初始化的全局变量,会把变量名指向一个之前定义过的存储位置(即其它文件),即告诉编译器遇到此变量或函数在其它模块中寻找其定义。
extern "C" {} 的作用是让编译器把代码块当成 C 语言处理,可以避免 C++ 因符号修饰导致代码不能和 C 语言库中的符号进行链接。

1
2
3
4
5
6
7
8
// 文件1
extern int count;
extern void write();

int main()
{
write();
}
1
2
3
4
5
6
// 文件2
int count = 5;
void wirte()
{
std::cout << count++ << std::endl;
}

mutable

mutable 可变的;constant 固定的。

mutableconst 的反义,是为了突破 const 限制而设置的,被 mutable 修饰的变量,将永远处于可变的状态,即使在一个常函数中。如果类的成员函数不能修改对象的状态,那么这个成员函数一般会定义成常函数;但是有时候需要在常函数中修改一些跟类状态无关的数据成员,这时候把这个成员变量定义成 mutable 存储类即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Foo
{
public:
void output() const
{
std::cout << "test" << std::endl;
mTimes++;//error
mTimes2++;
}

private:
int mTimes;
mutable int mTimes2;
};