C++primer浅学笔记

变量

int a=0; int a={0}; int a{0}; int a(0);

无论初始化对象或是为对象赋值。但注意不允许初始值存在丢失风险:

double pi=3.1415; int b={pi};

定义于任何函数之外的内置类型变量被初始化为0,函数体内部(包括main函数!!)的内置类型变量不被初始化。

数组赋初值:

int a[4]{0},b[4]={0};    //内置类型数组初始化
int pi=3,c[4]={pi};
A d[4];                 //类A默认构造函数初始化
A e[4](A(2));           //用构造函数A(int)直接初始化每个对象

C++支持分离式编译,即程序若干文件可被独立编译。
C++是一种静态类型语言,其含义是在编译阶段检查类型。

声明使名字为程序所知,定义负责创建与名字关联的实体。若想声明一个变量而非定义它,则在变量前添加关键字extern且不赋初值,否则为声明并定义—若给extern关键字标记的变量赋初值,则extern无用。

extern int i; //声明i而非定义i int j; //声明并定义j int a=0; //显示初始化即成定义

在函数体内部,若试图初始化一个由extern关键字标记的变量,将报错。

变量能且只能被定义一次,但可以被多次声明

extern表示该变量已经在别处(一般为文件外的某全局变量)定义过了,在这里是要使用那个变量。

用户自定义的标识符(变量名)不能连续出现两个下划线,不能以下划线紧连大写字母开头,定义在函数体外的标识符不能以下划线开头。
全局(静态)变量名与局部变量名相同,局部作用域内使用局部变量。

引用与指针

引用即为对象起别名,引用类型引用另外一种类型。引用与初始值对象一直绑定在一起,而非拷贝初始值。
int a=0,&i=a;

注意引用类型要和与之绑定的对象严格匹配,且只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起,以下为错误定义

int &a=3; double b=3.14; int &a=b; int &a

引用必须初始化!引用本身并非一个对象,一旦定义了引用,就无法再绑定到另外的对象,之后每次使用这个引用都是访问它最初绑定的那个对象。但注意允许多引用传递:

int i=1,&j=i,&k=j;           //j、k均为i的别名

指针*p与引用&a区别:
1. 指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。
2. 指针无须再定义是赋初值。

取地址符&b与解引用符*p:

int b=1,*p=&b; //取地址 cout<<*p; //解引用
  1. &随类型名出现,a是一个引用。&出现在表达式,b是一个取地址符。
  2. 不能定义指向引用的指针,因为引用不是对象,没有实际地址。
  3. 指针的类型必需与指向对象的类型匹配,否则报错。
  4. 不能把变量直接赋给指针,即使变量值等于0!
  5. 解引用操作仅适合于那些确实指向了某个对象的有效指针
int i=1,&j=i,*k=&j;       //通过且j和*k是i的别名,i,j,k均为同一地址
int *p=0;                 //错误!正确赋值int *p=&i;

指针的值(即地址)4个状态:
1. 指向一个对象
2. 指向紧邻对象所占空间的下一个位置
3. 空指针,意味着指针没有指向任何对象
4. 无效指针,即上述3种之外的其它值,不得拷贝或访问
第2种和第3种行为下的指针同样不允许试图访问。

空指针

int *p=nullptr; int *p=0; int *p=NULL;//需要cstdlib库(?存疑,一般没有也能用)

这里nullptr是一种特殊类型的字面值,它可以被转换成任意其它的指针类型。NULL为预处理变量,值为0。

预处理变量由预处理器负责管理,预处理器是运行与编译过程之前的一段程序,当用到一个预处理变量时,预处理器会自动地将它替换为实际值。预处理变量无视作用域规则!

任何非空合法指针对应的条件值都是true。

void* 指针了存放任意非const类型任意非常量对象的地址,但不能直接操作void*所指向的对象,必须强制类型转换。

引用不是对象,故而不能定义指向引用的指针。但指针是对象,故存在对指针的引用:

int *p=&i,*&r=p; //表示r是一个对指针p的引用
int *p=&i,&c=*p;//表示c是*p的别名
int i=36; r=&i; //表示r引用了一个指针,此处即为令p指向i
*r=0; //解引用得到i,即p指向的对象,将i的值改为0

const限定符

const对象一旦创建后其值就不能再改变,所以const对象必须初始化,但初始值可以为任意复杂的表达式,比如:
const int i=get_number();

注:const对象能完成大部分非const对象所能参与的操作,比如可将const对象赋给非const对象!主要限定在于不能改变其内容。例如const int和普通的int一样都能参与算术运算,也都能转换成一个布尔值,赋值给其它变量(不论是否为常量)。

编译器在编译过程中把用到常量的地方都替换成对应的初始值!

若想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字。

// file1.cpp 定义并初始化一个常量
extern const int s=get_number();
// file2.h 访问了常量
extern const int s; //与file1.cpp中定义的s是同一个

把引用绑定到const对象上,称之为对常量的引用

const int a=1;
const int &b=a; //正确,引用及其对应的对象都是常量
b=4; //错误,对常量的引用不能修改它所绑定的对象
int &c=a; //错误,不允许一个非常量引用指向一个常量对象

初始化常量引用允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。这意味着,允许一个常量引用绑定非常量的对象、字面值、甚至一般表达式:

int i=4;
const int &a=i; //允许将const int &绑定到一个普通int上,但a不允许修改i的值!i可以修改自己的值
const int &b=4; //正确
const int &c=a*2; //正确
int &d=a; //错误,非常量引用不能赋值常量引用

对于数组:

const int arr[2]{1,2};

其中值arr[0]和地址arr都不可修改!但为形参时退化为const int *arr ,此时地址arr可修改

注:

double j=3.14;
const int &e=j; //能运行,double可以转换成int类型。但非法

这里为了确保让e绑定一个整数,编译器把上述代码变成了:

const int temp=j; //由double生成一个临时的整型常量
const int &e=temp; //让e绑定这个临时量

注意此时e绑定的是一个临时量而非j!

指向常量的指针(const int *p)允许指向非常量对象但不能通过自己改变对象的值,另外不允许出现const int *p=&2&(i+2)

常量指针是把*放在const关键字之前,这时指针是一个常量,不变的是指针本身的值(即指向的地址),而非其所指向的对象的值保持不变。能否通过指针(*p)修改其所指向的对象的值完全取决于所指向对象的类型(即是否为非常量)。

定义顶层const表示指针本身是一个常量,底层const表示指针所指的对象是一个常量。

常量表达式是指不会改变且在编译过程中就能得到计算结果的表达式。constexpr限定在编译期常量但constexpr函数返回值不一定为编译器常量。包括字面值和用常量表达式初始化的const对象。

const int a=20; //是
const int b=a+1; //是,且无论a是否为常量
int c=20; //否,数据类型非const
const int d=get_number(); //否,具体值直到运行才能获取到

若需要一个变量为一个常量表达式,那就把它声明成constexpr类型。

constexpr int a=20 //是
constexpr int b=a+1;//是,且无论a是否为常量
constexpr int c=get_number();//只有当get_number()是constexpr函数时正确

注:constexpr仅支持算术类型、指针和引用(三者属于字面值类型),而自定义类如string,IO库等则不能。另外constexpr指针的初始值必须是nullptr或0,或存储在固定地址中的对象—–这意味着函数体内的变量一般不能被constexpr指针指向。

再注:constpr限定符仅对指针本身有效,对指针所指向的对象无关:

const int *p=nullptr;//p是一个指向整型常量的指针
constexpr int *q=nullptr;//q是一个指向整数的常量指针

constexpr函数的返回值及所有形参的类型都得是字面值类型且函数体中必须有且只有一条return语句,但其返回值不要求一定是常量表达式。constexpr函数内声明的所有变量必须初始化,且只有当实参是常量表达式时返回值才是常量表达式。

类型

类型别名typedef与using

typedef int a; //a是int的同义词
typedef a b,*p; //b是int的同义词,p是int*的同义词—是指向int的指针
using x=y;x是y的同义词

auto类型说明符让编译器通过初始值来推算变量的类型,故而auto定义的变量必须有初始值。可以在一条auto语句中声明多个变量,但该句中所有变量的初始基本数据类型必须一样。
使用auto时注意其不能识别引用和顶层const,即以下情况:

const int i=10;
auto a=i;//此时b被定义成了一个int类型
const auto b=i;//此时b才被定义成了一个const int类型
auto c=&i;//c是一个指向整数常量的指针

类型说明符decltype选择并返回操作数的数据类型,注意编译器只分析表达式并得到它的类型却不会实际计算表达式的值。decltype接受函数名、变量名、表达式、const和引用。

函数
decltype(func()) a=b;//a的类型就是函数func的返回类型,但编译器不实际调用func
变量名
decltype(i) c=0;//c的类型是i的数据类型即const int
const int &j=i;
decltype(j) d=c;//d的类型是const int&引用,d绑定到c
表达式
int a=1,*p=&i,&r=i; double b=3.14;
decltype(a+b) c;//c的类型是表达式a+b计算结果对应的double类型
decltype(r+0) d=0;//d的类型是int非引用
decltype(*p) e=0;//e的类型是引用int&而非int

注:decltype所用表达式加括号时编译器当成表达式,不加时才表示变量类型。

decltype(a) x;//x的类型是a的类型int
decltype((a)) y=0;//y的类型是引用int&(双层括号的结果永远是引用!!)

sizeof运算符:
1. 对引用类型执行sizeof运算得到被引用对象所占空间的大小
2. 对解引用指针执行sizeof运算得到指针指向的对象所占空间的大小

显示转换
在隐式类型转换中编译器只会执行一步类型转换,若程序隐式的使用了两种转换规则则会报错。

cast-name<type>(expression)
cast-name:static_castdynamic_castconst_castreinterpret_cast
  1. static_cast:任何具有明确定义且不包含底层const的类型转换。可用于编译器无法自动执行的类型转换(如void*转换为int*)。
  2. const_cast:只能改变运算对象的底层const。(去const性质:将常量对象转换成非常量对象。只允许const_cast强制转换)
const int a=10;
int *p=const_cast<int *>(&a);
int &r=const_cast<int &>(a);
*p=20,r=30;        //非常量对象赋值
cout<<a<<*p<<r;    //a=10,*p=20,r=30但a、p、r均指向同一地址!
  1. dynamic_cast:运行时编译器执行类型识别。支持子类到父类的转换,若父类到子类,则应为父类指向子类的指针或引用。包括以下三种形式

    dynamic_cast<type*>(e)//指针类型,e必须是一个有效的指针
    dynamic_cast<type&>(e)//引用类型,e必须是一个左值
    dynamic_cast<type&&>(e)// 引用类型,e不能是一个左值

    type必须是一个类类型,且通常情况下应含有虚函数。
    e的类型是三者之一:目标type的公有派类、目标type的公有基类、目标type的类型。
    注:左值指的是既能够出现在等号左边也能出现在等号右边的变量(或表达式),右值指的则是只能出现在等号右边的变量(或表达式),即不能赋值的

  2. reinterpret_cast:支持任何转换,但不会对指针的值进行调整,它是“位”上的重新解释。比如int * 转换为long *

静态局部对象(static)在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,即使对象所在函数执行结束也不会对它有影响。但其使用范围仍限于作用域内。
若局部静态变量没有显示的初始值,它将执行值初始化,内置类型的局部静态变量初始化为0。

函数

函数重载中形参为const时忽略顶层const,如下:

void func(const int i){}//func可以读取i但不能修改i
void func(int i){}//错误,重复定义func(int)

尽量使用常量引用!!
我们不能把const对象、字面值或需要类型转换的对象传递给普通的引用形参。

由于数组的两个性质:
1. 不允许拷贝数组
2. 使用数组时(通常)会将其转换成指针
若我们传给函数的是一个数组,则实参自动地转换成指向数组首元素的指针,数组的大小对函数的调用没有影响。

数组的首元素和尾元素函数
#include<iterator>//需要iterator头文件
int arr[]={0,1,2,3,4,5,6,7,8,9};
int *first=begin(arr),*last=end(arr);//首元素和尾元素的下一个元素

范围for语句
for(auto &i : arr){}//若需要对序列中的元素执行写操作,循环变量必须声明成引用类型。否则不用。

常量成员函数(注意此处的const不可改为constexpr,会报错)

class_name::func() const {}//const用于修改隐式this指针的类型

this本身是一个常量指针,指向对象本身的地址。紧跟在参数列表后面的const表示this是一个指向常量的指针,这意味着我们不能改变调用它的对象的内容,如下就是错误的:

string::count() const {length++;}//错误,这里length为string数据成员,不能在常量成员函数里修改数据成员,只可调用。
特例:
mutable int length;//mutable表示可变数据成员
string::count_mut() const {length++;}//正确,const成员函数可以改变一个可变成员的值

常量对象、以及常量对象的引用或指针都只能调用常量成员函数。

this作用域在类内部,当在类的非静态成员函数中访问类的非静态成员时,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。对于任何类成员的直接访问都被看成this的隐式使用。

要求编译器生成默认构造函数

class_name () = default;

构造函数初始化列表:

line::line(int start,int end):first(start),second(end) {}

若成员是const、引用或属于某种未提供默认构造函数的类类型,必须通过构造函数初始化列表为这些成员赋初值。
成员的初始化顺序与它们在类定义中的出现顺序一致,而与初始化列表中成员的出现顺序无关。

若需要限制构造函数定义的隐式转换,应使用关键字explicit

explicit class_name(const int&);

explicit关键字只对一个实参的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换。且只能在类内声明构造函数时使用explicit关键字,在类外部定义时不应重复。且explicit构造函数只能用于直接初始化。

如果使用等号(=)初始化一个变量,实际上执行的是拷贝初始化;
若不使用等号,则执行的是直接初始化。

聚合类:
1. 所有成员都是public的
2. 没有定义任何构造函数
3. 没有类内初始值
4. 没有基类,也没有virtual函数
聚合类使得用户可以直接访问其成员,并且具有特殊的初始化语法形式。如下是一个聚合类:

struct Data {
    int first;
    string second;
};

我们可以提供一个花括号的成员初始值列表,并用它初始化聚合类的数据成员:

Data a={0,"SECOND"};//初始值的顺序必须与声明的顺序一致

前置constexpr关键字声明constexpr构造函数,用于生成constexpr对象及constexpr函数参数或返回类型,其必须初始化所有数据成员,其初始值或使用constexpr构造函数或是一条常量表达式。

注意构造函数不能是const的!

类的静态成员static
类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。其类型可以是常量、引用、指针、类类型等。类似的,静态成员函数也不与任何对象绑定在一起,它们不包含this指针,故而静态成员函数不能声明成const的,且我们也不能再static函数体内使用this指针。
我们可以使用作用域运算符直接访问静态成员,但我仍然可以通过类的对象、引用或指针来访问:

static double num;//声明string类中的静态数据成员num
double a=String::num;//通过类名直接访问静态成员
String s; a=s.num;//通过类的对象访问静态成员

不能在类的内部初始化静态成员(除const),必须在类的外部定义和初始化每个静态成员且只能定义一次。类似全局变量,静态数据成员定义在任何函数之外,因此一旦被定义,就将一直存在于程序的整个生命周期中。

double String::num=0;//类外定义并初始化一个静态成员

可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr,初始值必须是常量表达式。

static constexpr int cnum=10;//在类内初始化,初始值必须是常量表达式

如果在类内提供了一个初始值,则成员定义不能再指定一个初始值了。但即使一个常量静态数据成员在类内部被初始化了,通常情况下也应该在类的外部定义一下该成员:

constexpr int String::cnum;//类外定义,但初始值类内定义提供

静态成员可以作为成员函数的默认实参,因为静态成员独立于任何对象;但普通成员不行,因为普通成员的值本身属于对象的一部分。另外静态数据成员的类型可以是它所属的类类型,而非静态数据成员则受到限制,只能声明成它所属的指针或引用:

static String sstr;//正确,静态成员可以是不完全类型
String *pstr;//正确,指针成员可以是不完全类型
String str;//错误,数据成员必须是完全类型

动态内存

C++内存划分为4个区域:
1. 静态/全局内存:存储全局变量及静态局部变量,在使用之前分配,在程序结束时销毁。
2. 栈内存:保存定义在函数内的非static对象,仅在其定义的程序块运行时才存在。静态和栈中的对象均由编译器自动创建和销毁。
3. 堆/自由空间:存储动态分配的对象即那些在程序运行时分配的对象。其生存期有程序来控制,当动态对象不再使用时,必须在代码中显式地销毁它们。
4. 常量存储区:存储不允许进行写操作的常量表达式。

new/delete
经典:new在动态内存中为对象分配空间并返回一个指向该对象的指针。delete接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。delete释放一个空指针总是没错的。

注意用new分配const对象是合法的:

const int *p=new const int(10);           //分配并初始化一个const int 
delete p;                                 //同样使用delete释放一个const动态对象也是合法的
const int *q=new const int[3]{1,2,3};  
delete [] q;

当我们使用一条new 表达式时:
1. new表达式调用一个名为operator new(或operator new[])的标准库函数。该函数分配一块足够大、原始的、未命名的内存空间以便存储待定类型的对象(或对象的数组)。
2. 编译器运行相应的构造函数以构造这些对象,并为其传入初始值。
3. 对象被分配了空间并构造完成,返回一个指向该对象的指针。

当我们使用一条delete表达式删除一个动态分配的对象时:
1. 对p所指的对象或者所指的数组中的元素执行对应的析构函数。
2. 编译器调用名为operator delete(或operator delete[])的标准库函数释放内存空间。

由new管理的动态内存在被显式delete释放前一直都会存在!!

智能指针:是模版,其行为类似常规指针,但它负责自动释放所指向的对象。包括shared_ptr 允许多个指针指向同一个对象,unique_ptr 则独占所指向的对象。除此之外还有weak_ptr 弱引用指向shared_ptr所管理的对象。

智能指针需要#include<memory>

shared_ptr

shared_ptr<int> p;//默认初始化的智能指针中保存着一个空指针。
shared_ptr<int> q(new int(12));//初始值为12的int类型的智能指针,注意是直接初始化形式!
q=new int(12);//错误!接受指针参数的智能指针构造函数是explicit的,因此我们不能将一个内置指针隐式的转换为一个智能指针

一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。若将智能指针绑定到一个指向其它类型的资源的指针上,必须提供自己的操作来替代delete。

shared_ptr<int> p(r,d);//p接管了内置指针r所指向的对象的所有权,r必须能转换为int* 类型,p将使用可调用对象d来代替delete来释放对象。
可调用对象:对于一个对象或一个表达式,若可以对其使用调用运算符,则称它为可调用的。
调用运算符():一对圆括号,里面放置实参列表,用于调用一个函数。

智能指针中的常用函数

p.get();//返回p中保存的指针
swap(p,q);//交换p和q的指针
p.use_count(); //返回与p共享的智能指针数量
p.reset(); //若p是唯一指向其对象的shared_ptr,reset会释放此对象,将p置为空。
p.reset(q); //另p指向q,并释放p所指向的对象

最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数,此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。

shared_ptr<int> q=make_shared<int>(12);//指向一个值为12的int 的shared_ptr
shared_ptr<string> str=make_shared<string>(5,'9'); //返回一个值为”99999”的string,传递的参数必须与string的某个构造函数相匹配
auto r=make_shared<vector<string>>();//指向一个动态分配的空string向量,使用auto来保存结果

当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其它shared_ptr指向相同的对象

auto p=make_shared<int>();//p指向的对象只有p一个引用者
auto q(p);//p和q指向相同对象,此对象有两个引用者

可认为每个shared_ptr都有个关联的计数器,称引用计数。无论何时拷贝一个shared_ptr时,计数器都会递增。当给一个shared_ptr赋予一个新值或shared_ptr被销毁时,计数器会递减。一旦一个shared_ptr的计数器变为0即最后一个shared_ptr被销毁时,它就会自动释放自己所管理的对象。

auto r=make_shared<int>();//r指向的int只有一个引用者
r=q;//给r赋值令它指向另一个地址->递增q指向的对象的引用计数->递减r原来指向对象的引用计数->r原来指向的对象已没有引用,会自动释放

注意只要有其它shared_ptr指向这块内存,它就不会被释放掉:

shared_ptr<int> get_number(const int arg) {
    shared_ptr<int> p=number(arg);
    return p; //当我们返回p时,引用计数进行了递增操作,因为return语句向此函数的调用者返回了一个p的拷贝
} //p离开了作用域,但它指向的内存不会被释放

此时我们就可以使用动态内存来允许多个对象共享相同的状态,从而避免以下状况:

vector<string> str1;
{ //新作用域
    vector<string> str2={"1","2","3"};
    str1=str2; //从str2拷贝元素到str1中    
} //str2被销毁,其中的元素也被销毁。但str1中有三个元素,是str2中元素的拷贝

现在我们只需要把数据成员声明成shared_ptr即可:

shared_ptr<vector<string>> data;
并把创造函数声明成如下即可:
String::String(initializer_list<string> il):data(make_shared<vector<string>>(il)) {}
//使用make_shared<vector<string>>(il) 初始化数据成员
对于拷贝、赋值和销毁使用默认版本的即可。

不要使用一个内置指针来访问一个智能指针所负责的对象!如下函数:

void process(shared_ptr<int> ptr){
    //使用ptr
} //ptr离开作用域并销毁

注意process的参数是传值方式传递的,因此实参会被拷贝到ptr中,从而递增其引用计数为2,但当process结束时,ptr计数递减但不为0,当局部变量被销毁时,ptr指向的内存不会被释放。当我们传递一个普通指针而非shared_ptr时:

int *x(new int(12)); //x是一个普通指针非智能指针
process(x); //错误!int*无法隐式转换为shared_ptr<int>
process(shared_ptr<int>(x)); //合法,但内存会被释放
int j=*x; //未定义的:x是一个空悬指针!

unique_ptr
注意某个时刻只能有一个unique_ptr指向一个给定的对象,当unique_ptr被销毁时,它所指向的对象也被销毁。另外unique_ptr没有make_shared函数:

unique_ptr<double> p(new int(12)); //p指向一个值为12的double

由于unique_ptr拥有它所指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作:

unique_ptr<double> q(p); //错误!unique_ptr不支持拷贝
unique_ptr<double> r; r=p; //错误!unique_ptr不支持赋值

unique_ptr常用函数:

u=nullptr; u.release(); u.reset(); //释放u所指的对象,并置u为空
u.reset(q); // 另u指向内置指针q

reset成员接受一个可选的指针参数,令unique_ptr重新指向给定的指针,若非空则还会释放原来指向的对象。release会切断unique_ptr和它原来管理的对象间的联系,其返回一个指针通常用于初始化或赋值给另一个智能指针。

不能直接拷贝和赋值unique_ptr但可以有一个例外:我们可以拷贝或赋值一个将要被销毁的不能直接拷贝和赋值unique_ptr,比如函数返回一个unique_ptr、返回一个局部对象的拷贝、通过release和reset将指针的所有权从一个(非const)unique_ptr转移到另一个:

unique_ptr<int> clone(int p) {return unique_ptr<int>(new int(p));} //函数返回值
unique_ptr<int> clone(int p) {unique_ptr<int> res(new int(p)); return res;} //返回局部对象的拷贝
unique_ptr<string> q(p.release()); //release函数
q.reset(p.release()); //reset函数
auto r=p.release(); //正确!但必须记得delete(r),此处r为int*内置指针类型

unique_ptr管理动态数组,不用提供自定义删除器:

unique_ptr<int[]> arr(new int [10]); //arr指向一个包含10个未初始化int的数组
int x=arr[2]; //读取数组中的数据,通过[]操作符访问
arr.release();arr.reset() //自动用delete[]销毁其指针

但注意指向数组的unique_ptr不支持成员访问(.和->),其它操作不变。再注意shared_ptr不直接支持管理动态数组,若希望其支持,必须提供自己定义的删除器:

shared_ptr<int> arr(new int[10], [](int *p){delete[] p;});   //第二个参数为自定义的删除器函数
shared_ptr<const int> arr(new const int[2]{1,2}, [](cosnt int *p){delete[] p;});
int y=*(arr.get()+2);    //shared_ptr不支持[]及指针的算术运算,这里我们使用get获取一个内置指针
arr.reset();    //使用自定义删除器销毁数组

weak_ptr
weak_ptr是一种不控制所指向对象生存周期的智能指针,它指向一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数,一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也还是被释放。

我们创建一个weak_ptr时,要用一个shared_ptr来初始化它:

auto p=make_shared<int>(12); weak_ptr<int> wp(p);//wp弱共享p,p的引用计数未改变

weak_ptr常用函数:

wp.reset(); //将wp置为空
wp.use_count(); //与wp共享对象的shared_ptr的数量
wp.expired(); //若wp.use_count()为0返回true,否则false
wp.lock(); //若wp.expired()为true返回空shared_ptr,否则返回一个指向wp对象的shared_ptr

由于对象可能不存在,我们不能直接使用weak_ptr访问对象,而必须调用lock:

if(shared_ptr<int> np=wp.lock()) {} //若np不为空则条件成立,并使用np访问共享对象

关于auto_ptr的不足
会转移指针托管权,也就是拷贝原始指针后会令原指针为NULL。另外还有如下缺点:

  • 不能共享所有权
  • 不能指向数组
  • 不能作为容器成员
  • 不能通过拷贝(=)初始化
  • 构造函数是explicit的

allocator分配内存
new/delete将内存分配、销毁和对象构造、析构组合在了一起,导致了不必要的浪费。使用allocator类可以将内存分配和对象构造分离开来。当一个allocator对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对齐位置。

allocator<int> alloc; //可以分配int的allocator对象
auto const p=alloc.allocate(n); //分配一段原始的、未构造的内存,保存n个类型为int的对象,返回一个int* 指针
auto q=p; //q指向最后构造的元素之后的位置
alloc.construct(q++,args); //args是被传递给int的构造函数参数列表,用来在p指向的内存中构造一个对象。args为空即调用默认构造函数
int z=*(--q); //构造对象后方能使用,注意z=*q; 是错误的!q指向的是未构造的内存
alloc.destroy(q); //对q所指向的对象(真正构造了的元素)执行析构函数,之后可以选择重新使用这部分内存保存其它int
alloc.deallocate(p,n); //释放从p地址开始的内存,这块内存保存了n个类型为int的对象,参数必须为allocate中的p与n,且在之前必须先对每个在这块内存中创建的对象调用destroy

拷贝控制

包括拷贝构造函数、拷贝赋值运算符(operator=)、析构函数。
定义为=default 显式要求编译器生成默认合成的版本。且类内修饰会声明为内联函数,类外不会。

=default直到编译器生成代码时才需要。

什么时候产生临时变量:

  • 传值参数
  • const参数
  • 类型转换

什么时候会发生拷贝初始化:

  • 用“=”定义变量
  • 将一个对象作为实参传递给一个非引用类型的对象
  • 将一个返回类型为非引用类型的函数返回给一个对象
  • 用花括号列表初始化一个数组中的元素或一个聚合类中的成员

什么时候会调用析构函数:

  • 变量在离开其作用域时
  • 当一个对象被销毁时,其成员被销毁
  • 容器(无论是标准库容器还是数组)被销毁时,其元素被销毁
  • 对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁
  • 对于临时对象,当创建它的完整表达式结束时被销毁

总的来说,无论何时一个对象被销毁,就会自动调用其析构函数。

在一个构造函数中,初始化对象的非static数据成员,还可能做一些其他工作,成员的初始化是在函数体执行之前完成的,且按照它们在类中出现的顺序进行初始化。在一个析构函数中,释放对象使用的资源,并销毁对象的非static数据成员,首先执行函数体,然后销毁成员—-这就意味着析构函数体自身并不直接销毁成员,而是在函数体后隐含的析构阶段被销毁!

析构函数的成员销毁完全依赖于成员的类型,销毁类类型成员(包括智能指针)需要执行成员自己的析构函数,而内置类型什么都不用做。注意析构函数是隐式的,一般不用特意控制成员如何销毁。

与默认合成构造函数不同,即使我们定义了其它构造函数,编译器仍会为我们合成一个合成拷贝构造函数/合成拷贝赋值运算符/合成析构函数。

三五法则:需要析构函数的类也需要拷贝和赋值操作,反之不成立;需要拷贝操作的类也需要赋值操作,反之亦然。

删除的函数:虽然声明了它们,但不能以任何方式使用它们。通过在参数列表后加上=delete实现。

String(const String&)=delete;//阻止拷贝
String &operator=(const String&)=delete; //阻止赋值

=delete必须出现在函数第一次声明的时候,通知编译器不要定义这些成员,禁止试图使用删除函数的操作。可以对非构造、析构函数外的任何函数使用。

注意不能删除析构函数!若删除了析构函数,编译器将不允许定义该类型的变量或创建该类的临时变量。另外若类中有成员的析构函数是删除的,由于不能销毁该成员导致对象整体也不能被销毁,编译器也会阻止定义该类的变量或临时对象。

对于析构函数已删除的类型,不能定义该类型的变量或释放指向该类型动态分配对象的指针。(尽管编译器不会阻止)

~noDtor()=delete; noDtor *p=new noDtor(); //正确但不能delete

编译器定义为删除的函数:
1. 若类的某个成员的析构是删除或不可访问的,则该类的默认合成析构也被定义为删除的。
2. 若类的某个成员的拷贝构造或析构是删除或不可访问的,则该类的默认合成拷贝构造函数也被定义为删除的。
3. 若类的某个成员的拷贝赋值是删除或不可访问的、或类有一const的引用成员,则该类的默认合成拷贝赋值运算符也被定义为删除的。
4. 若类的某个成员的析构函数是删除或不可访问、或有引用成员没有类内初始值、或类内有const成员没有初始值且类型未显式定义默认构造函数,则该类的默认构造函数被定义为删除的。

本质上,若一个类有成员不能默认构造、拷贝、复制或销毁,则对应的成员函数将被定义为删除的。

注:经典方法是通过将其拷贝构造函数和拷贝赋值运算符声明为private来阻止拷贝。

右值引用&&:必须绑定到右值的引用。右值引用只能绑定到一个将要销毁的对象,因此我们可以自由的将一个右值引用的资源“移动”到另一个对象上。

常规(左值)引用&:不能将其绑定到要求转换的表达式、字面常量或是返回右值的表达式。—当然右值可以。
int i=12;
int &r=i; //正确,r引用i
int &&rr=i; //错误,不能将一个右值绑定到一个左值上
int &&rr2=12; //正确,字面值常量是右值
int &r2=i*42; //错误,i*42是一个右值
const int &r3=i*42; //正确,我们可以将一个const的引用绑定到一个右值上
int &&rr3=i*42; //正确,将右值绑定到乘法结果上

右值引用虽然不能直接绑定到左值上,但可以使用move函数显式转换:

int &&rr4=std::move(i); //需要#include<utility>,且之后不允许再使用i的值。

面向对象编程

面向对象程序设计的核心思想是数据抽象、继承和动态绑定。
数据抽象可以将类的接口与实现分离。继承可以定义相似的类型并对其相似关系建模。动态绑定可以在一定程度上相似类型的区别,而以统一的方式使用它们的对象。

继承:基类、派生类。派生类必须通过类派生列表明确指出它是从哪个基类继承而来,且基类必须已经定义而非仅仅声明。

class Derived:public Base {};

动态绑定:运行时绑定,函数的运行版本有实参决定。

当我们使用基类的引用(或指针)调用一个虚函数时将发生动态绑定。

基类必须将它的两种成员函数区分开来:一种是基类希望其派生类进行覆盖的函数(通常定义为虚函数virtual),另一种是基类希望派生类直接继承而不要改变的函数。

virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义。
成员函数若没有被声明为虚函数,则其解析过程发生在编译时而非运行时。

派生类必须将其继承来的成员函数中需要覆盖的那些重新声明(并不是指虚函数)。若派生类没有覆盖其基类中的某个虚函数,则该虚函数的行为类似于其它的普通成员,派生类会直接继承其在基类中的版本。派生类可以显式地注明它使用某个成员函数覆盖了它继承的虚函数:在形参列表后面、或const成员函数的const后面、或引用成员函数的引用限定符&/&&后面添加一个关键字override

引用成员函数:
String func() const &; //&指出this只能指向一个左值。
引用限定符&、&&只能用于非static成员函数,且必须同时出现在函数的声明和定义中。且只能跟随在const后。

派生类首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。派生类声明中只需要包含类目无需包含它的派生列表,定义中方需添加派生列表。且一个类不能派生它自身。派生类会继承直接基类的所有成员。派生类构造函数只会初始化它的直接基类。

若基类定义了一个静态成员,则在整个继承体系中只存在该成员唯一定义,不论从基类中派生出来多少个派生类,对于每个静态成员来说都只存在唯一的实例。若静态成员是可访问的,则我们即可以通过基类也可以通过派生类使用。

类目后跟关键字final禁止其他类继承该类:

class Forbid final {}; //Forbid类无法被继承

不存在从基类向派生类的隐式类型转换,派生类向基类的自动类型转换只对指针或引用有效

注意一个基类的对象可能是派生类对象的一部分。
当我们用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,它的派生类部分将被忽略掉。

友元:类可以允许其它类或函数访问它的非公有成员。友元不是类的成员也不受它所在的区域访问控制级别的约束。每个类负责控制自己的友元类或友元函数,友元不能传递、继承。若声明友元的类的成员调用友元函数,该友元函数也必须是已经定义的。

class First { //令First类中的某个成员函数为友元
    void print() const; //首先定义First类并声明print函数
};
class Base {
    //友元在类内出现的位置不限,一般在类的开始或结束前集中声明
    friend int func() const;
    friend void First::print() const; //然后定义Base,包括对于print的友元声明
    friend class Second; //友元类类似
private:
    int x;
};
int func() const {Base a; return a.x;} //友元函数可以访问Base类的非公有成员
void First::print() const {Base b; cout<<b.x;} //最后定义print函数

虚函数
当我们使用基类的引用或指针调用一个虚成员函数时会执行动态绑定,因为我们直到运行时才能知道到底调用了哪个版本的虚函数,所有所有的虚函数都必须有定义,不管它是否被用到了,因为这是编译器也无法确定到底会使用哪个虚函数。另外一旦某个函数被声明成虚函数,则在所有派生类中它都是虚函数

对于存在继承关系的类型,存在两种类型:变量或其他表达式的静态类型、该表达式表示对象的动态类型。 静态类型在编译时是已知的,它是变量声明时的类型或表达式生成的类型。动态类型是变量或表达式表示的内存中的对象的类型。

当且仅当对通过指针或引用调用虚函数时才会在运行时解析该调用,被调用的函数是与绑定到指针或引用上的对象的动态类型相匹配的那一个。只有在此情况下对象的动态类型与静态类型不同。

面向对象程序设计的核心思想是多态性,把具有继承关系的多个类型称为多态类型。

当我们使用基类的引用或指针调用基类定义的一个函数时,我们并不知道该函数真正作用的对象是什么类型,因为它可能是一个基类的对象也可能是一个派生类的对象。如果该函数是虚函数,则直到运行时才会决定执行哪个版本,判断的依据是引用或指针所绑定的对象的真实类型。
另一方面,对非虚函数的调用在编译时进行绑定。类似的,通过对象进行的函数(虚函数或非虚函数)调用也在编译时绑定。对象的类型是确定不变的,通过对象进行的函数调用将在编译时绑定到该对象所属类中的函数版本上。

若派生类中的函数与基类中虚函数的名字相同但形参列表不同,编译器将认为这是一个新函数。派生类的函数必须覆盖掉基类的虚函数! 另外若override标记了某个函数,则必须覆盖已存在的虚函数。若final标记了某函数,则之后禁止覆盖该函数。

struct Base {
    virtual void f1(int=12) const; //虚函数允许使用默认实参
    virtual void f2();
    void f3();
    void f4() final;
};
struct Derived:Base {
    void f1(int) const override; //正确,f1与Base中f1匹配,且派生类使用基类中定义的默认实参
    void f2(int) override; //错误。Base没有f2(int)的函数
    void f3() override; //错误,f3不是虚函数    
    void f5() override; //错误,Base中没有名为f5的函数
    void f4(); //错误,Base中f4已声明为final
};

强行执行某个版本的虚函数(作用域运算符):

der->Base::f1(); //强行执行Base类中的虚函数,不管der是什么类型的对象

一般成员函数(或友元)中的代码、派生类的虚函数调用它覆盖的基类的虚函数时需要使用作用域运算符回避默认的虚函数机制。

为什么构造函数不能为虚函数?

  1. 虚函数对应一个vtable,而此vtable是存储在对象的内存空间中的。若构造函数是虚的,就需要通过vtable来调用,可对象还没有实例化,也就是内存空间还没有,无法找到vtable,所以构造函数不能是虚函数。vtable在构造函数调用后才建立。
  2. 虚函数的作用在于通过父类的指针或引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
  3. 创建一个对象时我们总是要明确指定对象的类型。

虚表指针位于类内存空间最前,其中父类的虚表指针按声明依次存储,最后存储本身的虚表指针。

纯虚函数:在函数体位置书写”=0”,只能在类内声明出现。一般无须定义,

void func() const=0; //类内声明
void Base::func() const {} //类外定义,无需”=0”

含有(或未经覆盖直接继承)纯虚函数的类是抽象基类。抽象基类负责定义接口,后续的其它类可以覆盖该接口。我们不能创建一个抽象基类的对象

通过在类的内部使用using声明语句,我们可以将该类的直接或间接基类中的任何可访问成员 标记出来,using声明语句中名字的访问权限由该using声明语句前的访问说明符决定。using声明语句是令某个名字在当前作用域内可见。

class Base {
public:
    int num; //基类中声明一个公有数据成员
    double price;
    void func() const ();
    void func(int) {};
    int func() const {};
};
class Derived: public Base {
private:
    using Base::num; //将派生类中继承基类的公有的num成员修改为private
public:
    double price; //隐藏了同名的基类成员(重用基类定义)  
    using Base::func; //把基类中名为func函数的所有重载实例添加到派生类的作用域中
};

使用struct和class关键字定义的类之间唯一的差别在于默认成员访问符和默认派生类访问说明符(struct均是默认public,class默认均是private)。

类中函数调用的解析过程(p->mem()或p.mem()):
1. 首先确定p的静态类型。
2. 在p的静态类型对应的类中查找mem。若找不到,则一次在直接基类中不断查找直到到达继承链的顶端。如果找遍了该类及其基类仍然找不到,则编译器报错。
3. 一旦找到了mem,就进行常规的类型检查以确定对于当前找到的mem,本次调用是否合法。
4. 假设调用合法,则编译器将根据调用的是否是虚函数而产生不同的代码:

  • 若mem是虚函数且我们是通过引用或指针进行的调用,则编译器产生的代码将在运行时确定到底运行该虚函数的哪个版本,依据的是对象的动态类型。
  • 反之,若mem不是虚函数或我们是通过对象进行的调用,则编译器将产生一个常规函数调用。

注意在不同的作用域中无法重载函数名,内层作用域中声明的函数名称将隐藏外层作用域中声明的同名实体(尽管形参列表不一致)

定义在派生类中的函数也不会重载其基类中的成员,若派生类的成员与基类的某个成员同名,则派生类将在其作用域内隐藏该基类成员。即使形参列表不一致,基类成员也会被隐藏掉。

struct Base { int func(); };
struct Derived:Base { int func(int); };
Base b; Derived d;
b.func(); //正确,调用的是Base::func
d.func(); //错误,参数列表为空的Base::func被隐藏了
d.Base::func(); //正确,调用的是Base::func
d.func(10); //正确,调用的是Derived::func

一个基类总是需要虚析构函数! 且此时我们并不一定需要拷贝和赋值操作。

class Base {public: virtual ~Base() =default;}; //动态绑定析构函数

注意子类不会自动继承父类的构造函数,派生类继承基类构造函数的方式是提供了一条注明了基类名的using声明语句:

class Derived:public Base {
public:
    using Base::Base;//使用基类的构造函数
};

与上面提到了using的用法不同,当作用于构造函数时,using声明语句将令编译器产生代码。对于基类的每个构造函数,编译器都生成一个与之对应的派生类构造函数即一个形参列表完全相同的构造函数。另外一个构造函数的using声明不会改变该构造函数的访问级别。且一个using声明不能指定explicit或constexpr,这意味着若基类的构造函数是explicit或constexpr的,则继承的构造函数也拥有相同的属性。

当一个基类构造函数含有默认实参时,这些实参并不会被继承,相反,派生类将获得多个继承的构造函数,其中每个构造函数分别省略掉一个含有默认实参的形参。

若基类含有几个构造函数,派生类会继承所有这些构造函数,除以下两种情况:

  • 派生类定义的构造函数与基类的构造函数具有相同的参数列表,则该构造函数不会被继承,定义在派生类的构造函数将替换继承而来的构造函数。
  • 默认构造、拷贝和移动构造函数不会被继承。编译器会默认合成。

注意我们不能把具有继承关系的多种类型的对象直接存放在容器中,一般来说,我们实际上存放的应当是基类的智能指针。

模版

template<typename T,class U=int> //类型参数,两个关键字含义相同,"=int"表示默认实参为int
template<unsigned N> //非类型参数,N必须是一个常量表达式,被实例化时N被该常量表达式替代。

当编译器遇到一个模版定义时,它并不生成代码,只有当我们实例化出模版的一个特定版本时,编译器才会生成代码。这意味着,为了生成一个实例化版本,编译器需要掌握函数模版或类模版成员函数的定义。

通常,当我们调用一个函数时,编译器只需要掌握函数的声明。类似的,当我们使用一个类类型的对象时,类定义必须是可用的,但成员函数的定义不必已经出现。

模版支持友元:

//前置声明,在将模版的一个特定实例声明为友元时用到
template <typename T> class Pal;
class C {
    friend class Pal<C>; //用类C实例化的Pal是C的一个友元
    tempalte<typename T> friend class Pal2; //模版类Pal2的所有实例都是C的友元,无须前置声明
    friend T; //将访问权限授予用来实例化C的类型
}

模版支持static:模版类的每个static数据成员必须有且仅有一个定义,但类模版的每个实例都有一个独有的static对象,且static成员函数只有在使用时才会实例化。

模版内不能重用模版参数名。若我们希望使用一个模版类型参数的类型成员,就必须显示告诉编译器该名字是一个类型。

typename T::type p; //定义一个T::type类型的变量p

显示模版参数:

template <typename T1,typename T2,typename T3>
T1 sum(T2,T3); //T1在最前,必须显示指定;T2,T3可函数实参推断而来
auto res=sum<long>(i,d); //long sum(int,double)
T3 sum2(T2,T1); //T3在最前,必须全部显示指定
auto res2=sum2<long,int,double>(i,d); //long sum2(int,double)

尾置返回:

template <typename T>
auto func(T bef,T end) ->decltype(*beg) { //返回类型为*beg参数类型
    return *beg;
}

特殊工具与技术

typeid(e)运算符获取表达式e的类型,得到一个常量对象的引用,该对象的类型是标准库类型type_info或其公有派生类。

if(typeid(*p)==typeid(Derived)){} //p实际指向Derived对象

枚举:当前枚举成员的值等于之前枚举成员的值加1,第一个默认为0

enum class color {red,yellow,blue}; //限定作用域的枚举类型
enum color {red,yellow,blue}; //不限定作用域的枚举类型,类名可不要
enum value:unsigned long {num1,num2,num3}; //将默认int整型表示的枚举成员改为unsigned long类型

枚举成员是const,初始值必须是常量表达式,每个枚举成员本身就是一个常量表达式

声明成员指针:

const string Base::*p; //指向一个Base类的(常量非常量)string成员的指针
p=&Base::data; //初始化成员指针
auto p=&Base::data; //最简单的声明并初始化成员指针的方法

嵌套类:定义在另一个类内部的类。嵌套类是一个独立的类,与外层类没有任何关系。在嵌套类的对象中不包含任何外层类定义的成员,外层类的对象中也包含任何嵌套类定义的成员。但嵌套类可以直接使用外层类的成员,外层类不能使用嵌套类的。可以声明static成员。

局部类:定义在一个函数内部的类。局部类只能访问外层作用域定义的类型名,静态变量以及枚举成员。若局部类定义在某个函数内部,则该函数的普通局部变量不能被该局部类使用。不允许声明static。

static关键字说明
1. static变量(无论全局、局部、类)在全局/静态内存区分配内存。
2. 静态变量只在第一次声明时初始化,未经初始化的静态变量会被程序自动初始化为0。
3. 静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的,也就是说该变量无法被extern。
4. 静态局部变量具有全局寿命,只第一次进入函数时被初始化,但只在局部作用域内可见。
5. 静态函数不能被其它文件所用。
6. 静态数据成员定义时要分配空间,不能在类内中定义,类体外定义不含static,由类的所有对象所共有。
7. 静态成员函数不与任何的对象相联系,不具有this指针,故而它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,它只能调用其余的静态成员函数。类体外定义不含static。不能将静态成员函数定义为虚函数。

在main函数前执行的代码:在main函数前定义全局变量、对象和静态变量、对象的空间分配和赋初值,在main前执行的函数。

全局对象的构造函数会在main 函数之前执行

在main函数后执行的代码:释放空间、释放资源使用权等操作。

全局对象的析构函数会在main函数之后执行

int func() {cout <<"func()" << endl; return 0;} //注意这里的返回值不能是void
class A {
public:
   A() {cout << "constructor" << endl;}
    ~A() {cout << "destructor" << endl;}
};
A a; //先实例化a
int x=func(); //然后执行函数,这里不能没有返回值,否则直接报错不能执行该语句
int main(void){
     cout << "main" << endl;
    return 0;
}

运行结果:

constructor //按照实例化顺序先声明的a,执行A的构造函数
func() //然后执行的是fun函数
main //然后执行main函数
destructor //最后main函数后销毁空间

链接指示extern “C”:
链接指示对整个声明都有效。

extern "C" void (*pf)(int) //pf指向一个C函数,该函数接受一个int并返回void
extern "C" void f1(void(*)(int)); //f1是一个C函数,它的形参是一个指向C函数的指针
extern "C" typedef void func(int); void f2(func *); //f2是一个C++函数,该函数的形参是指向C函数的指针

new和malloc的区别
1. new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件cstdlib支持。
2. 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸即需要n*sizeof(int)。
3. new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
4. new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。
5. new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现),然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)
6. C++允许重载new/delete操作符,特别的,布局new的就不需要为对象分配内存,而是指定了一个地址作为内存起始区域,new在这段内存上为对象调用构造函数完成初始化工作,并返回此地址。而malloc不允许重载。
7. new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。
8. new[]与delete[]来专门处理数组类型,使用new[]分配的内存必须使用delete[]进行释放。malloc就给你一块原始的内存。
9. 使用malloc分配的内存后,如果在使用过程中发现内存不足,可以使用realloc函数进行内存重新分配实现内存的扩充。

猜你喜欢

转载自blog.csdn.net/sinat_30477313/article/details/79781754