C++温习笔记(慕羽★)——指针及相关内容(下)
Posted 慕羽★
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++温习笔记(慕羽★)——指针及相关内容(下)相关的知识,希望对你有一定的参考价值。
本系列文章用于记录,近期温习C++过程中的一些笔记内容,本文主要记录指针相关的内容。
全部内容分为上下两篇
一、下篇目录:
- <font color=blue size=6 face="华文新魏">12、数组指针(行指针)
- <font color=blue size=6 face="华文新魏">13、动态创建数组
- <font color=blue size=6 face="华文新魏">14、结构体指针
- <font color=blue size=6 face="华文新魏">15、引用
- <font color=blue size=6 face="华文新魏">16、this指针
12、数组指针(行指针)
(1)一维数组
将一个整型变量加1后,其值将增加1。但是,将指针变量(地址的值)加1后,增加的量等于它指向的数据类型的字节数。
①数组在内存中占用的空间是连续的。
②C++将数组名解释为数组第0个元素的地址。
③数组第0个元素的地址和数组首地址的取值是相同的。
④数组第n个元素的地址是:数组首地址+n
⑤C++编译器把 数组名[下标] 解释为 *(数组首地址+下标)
数组是占用连续空间的一块内存,在多数情况下,数组名被解释为数组第0个元素的地址。C++操作这块内存有两种方法:数组解释法和指针表示法,它们是等价的。但是,将sizeof运算符用于数据名时,将返回整个数组占用内存空间的字节数。可以修改指针的值,但数组名是常量,不可修改。
示例12
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
int main()
int a[5] = 6 , 66 , 666 , 6666 , 66666 ;
//输出地址
cout << "a的值是:" << (long long)a << endl;
cout << "&a的值是:" << (long long)&a << endl;
cout << "a[0]的地址是:" << (long long)&a[0] << endl;
cout << "a[1]的地址是:" << (long long)&a[1] << endl;
int* p = a;
cout << "p的值是:" << (long long)p << endl;
cout << "p+0的值是:" << (long long)(p + 0) << endl;
cout << "p+1的值是:" << (long long)(p + 1) << endl;
//输出值
cout << "a[0]的值是:" << a[0] << endl;
cout << "a[1]的值是:" << a[1] << endl;
cout << "*(p+0)的值是:" << *(p + 0) << endl;
cout << "*(p+1)的值是:" << *(p + 1) << endl;
示例12输出结果
a的值是:557739014152
&a的值是:557739014152
a[0]的地址是:557739014152
a[1]的地址是:557739014156
p的值是:557739014152
p+0的值是:557739014152
p+1的值是:557739014156
a[0]的值是:6
a[1]的值是:66
*(p+0)的值是:6
*(p+1)的值是:66
一维数组用于函数的参数时,只能传数组的地址,并且必须把数组长度也传进去,除非数组中有最后一个元素的标志。
书写方法有两种:
void func(int* arr, int len);
void func(int arr[], int len);
在函数中,可以用数组表示法,也可以用指针表示法,不要对指针名用sizeof运算符,它不是数组名。
示例13
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
// void func(int *arr,int len)
void func(int arr[],int len)
for (int ii = 0; ii < len; ii++)
cout << "arr[" << ii << "]的值是:" << arr[ii] << endl; // 用数组表示法操作指针。
cout << "*(arr+" << ii << ")的值是:" << *(arr + ii) << endl; // 地址[下标] 解释为 *(地址+下标)。
int main()
int a[] = 2,8,4,6,7,1,9;
func(a, sizeof(a) / sizeof(int));
示例13输出结果
arr[0]的值是:2
*(arr+0)的值是:2
arr[1]的值是:8
*(arr+1)的值是:8
arr[2]的值是:4
*(arr+2)的值是:4
arr[3]的值是:6
*(arr+3)的值是:6
arr[4]的值是:7
*(arr+4)的值是:7
arr[5]的值是:1
*(arr+5)的值是:1
arr[6]的值是:9
*(arr+6)的值是:9
声明行指针的语法:数据类型 (*行指针名)[行的大小]; // 行的大小即数组长度。
几个行指针的示例如下:
int (*p1)[3]; // p1是行指针,用于指向数组长度为3的int型数组。
int (*p2)[5]; // p2行指针,用于指向数组长度为5的int型数组。
double (*p3)[5]; // p3是行指针,用于指向数组长度为5的double型数组。
一维数组名被解释为数组第0个元素的地址,若对一维数组名+1,得到的是数组中下一个元素的地址;对一维数组名取地址得到的是数组的地址,是行地址,若对一维数组名取地址+1,得到的是下一个数组的地址,如对于int a[10],&a+1得到的是数组a的地址+40,如下所示:
示例14:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
int main()
int a[10];
cout << "数组a第0个元素的地址:" <<(long long) a << endl;
cout << "数组a的地址:" << (long long)&a << endl;
cout << "数组a第0个元素的地址+1:" << (long long)(a + 1) << endl; // 地址的增加量是4。
cout << "数组a的地址+1:" << (long long)( & a + 1) << endl; // 地址的增加量是40。
示例14输出结果:
数组a第0个元素的地址:621223605816
数组a的地址:621223605816
数组a第0个元素的地址+1:621223605820
数组a的地址+1:621223605856
在上面的例子中,定义指针和行指针指向数组a的语法如下:
int* p1 = a; //指针
int(*p2)[10] = &a; //行指针
(2)多维数组
二维数组:int bh[2][3] = 11,12,13,21,22,23 ;
二维数组名是行地址,bh是二维数组名,该数组有2两元素,每一个元素本身又是一个数组长度为3的整型数组,bh被解释为数组长度为3的整型数组类型的行地址。如果存放bh的值,要用数组长度为3的整型数组类型的行指针。
int (*p)[3]=bh;
注意:不能使用 int *p=bh ,会报错
三维数组: int bh[4][2][3];
bh是三维数组名,该数组有4元素,每一个元素本身又是一个2行3列的二维数组。bh被解释为2行3列的二维数组类型的二维地址。如果存放bh的值,要用2行3列的二维数组类型的二维指针。
int (*p)[2][3]=bh;
其他高维数组以此类推、、、、、、
接下来看一下如何把二维数组传递给函数,如果要把上面的二维数组bh传给函数,函数的声明如下:
void func(int (*p)[3],int len);
void func(int p[][3],int len);
示例15:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
// void func(int(*p)[3], int len)
void func(int p[][3], int len)
for (int ii = 0; ii < len; ii++)
for (int jj = 0; jj < 3; jj++)
cout << "p[" << ii << "][" << jj << "]=" << p[ii][jj] << " ";
cout << endl;
int main()
int bh[2][3] = 77,66,99,77,88,66 ;
func(bh, 2);
示例15输出结果:
p[0][0]=77 p[0][1]=66 p[0][2]=99
p[1][0]=77 p[1][1]=88 p[1][2]=66
13、动态创建数组
在函数中普通数组在栈上分配内存,栈很小;如果需要存放更多的元素,必须在堆上分配内存。
动态创建一维数组的语法:数据类型 *指针=new 数据类型[数组长度];
释放一维数组的语法:delete [] 指针;
示例16
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
int main()
int *arr=new int[8]; // 创建8个元素的整型数组。
for (int ii = 0; ii < 8; ii++)
arr[ii] = 100 + ii; // 数组表示法。
cout << "arr[" << ii << "]=" << *(arr + ii) << endl; // 指针表示法。
delete[]arr;
示例16输出结果
arr[0]=100
arr[1]=101
arr[2]=102
arr[3]=103
arr[4]=104
arr[5]=105
arr[6]=106
arr[7]=107
–
①动态创建的数组没有数组名,不能用sizeof运算符。
② 可以用数组表示法和指针表示法两种方式使用动态创建的数组。
③必须使用delete[]来释放动态数组的内存(不能只用delete)。
④不要用delete[]来释放不是new[]分配的内存。
⑤ 不要用delete[]释放同一个内存块两次(否则等同于操作野指针)。
⑥ 对空指针用delete[]是安全的(释放内存后,应该把指针置空nullptr)。
⑦ 声明普通数组的时候,数组长度可以用变量,相当于在栈上动态创建数组,并且不需要释放。
⑧ 如果内存不足,调用new会产生异常,导致程序中止;如果在new关键字后面加(std::nothrow)选项,则返回nullptr,不会产生异常。
⑨ 为什么用delete[]释放数组的时候,不需要指定数组的大小?因为系统会自动跟踪已分配数组的内存。
14、结构体指针
结构体是一种自定义的数据类型,用结构体可以创建结构体变量。在C++中,用不同类型的指针存放不同类型变量的地址,这一规则也适用于结构体。如下:
struct st_muyu muyu; // 声明结构体变量muyu。
struct st_muyu *pst=&muyu; // 声明结构体指针,指向结构体变量muyu。
通过结构体指针访问结构体成员,有两种方法:
(*指针名).成员变量名 // (*pst).name和(*pst).age
指针名->成员变量名 // pst->name和*pst->age
在第一种方法中,圆点.的优先级高于*,(*指针名)两边的括号不能少。如果去掉括号写成(指针名).成员变量名,那么相当于(指针名.成员变量名),意义就完全不一样了。在第二种方法中,->是一个新的运算符。上面的两种方法是等效的,程序员通常采用第二种方法,更直观。与数组不一样的是,结构体变量名没有被解释为地址。
如果要把结构体传递给函数,实参取结构体变量的地址,函数的形参用结构体指针。如果不希望在函数中修改结构体变量的值,可以对形参加const约束。
15、引用
引用变量是C++新增的复合类型,引用是已定义的变量的别名。引用的主要用途是用作函数的形参和返回值。
引用的本质是指针常量的伪装,编译器会把引用解释为指针,引用和指针从本质上来说没有区别
声明/创建引用的语法:数据类型 &引用名=原变量名;
① 引用的数据类型要与原变量名的数据类型相同。
② 引用名和原变量名可以互换,它们值和内存单元是相同的。
③ 必须在声明引用的时候初始化,初始化后不可改变。
④ C和C++用&符号来指示/取变量的地址,C++给&符号赋予了另一种含义。
示例17
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
int main()
// 声明 / 创建引用的语法:数据类型 & 引用名 = 原变量名;
int a = 3; // 声明普通的整型变量。
int& ra = a; // 创建引用ra,ra是a的别名。
cout << " a的地址是:" << &a << ", a的值是:" << a << endl;
cout << "ra的地址是:" << &ra << ",ra的值是:" << ra << endl;
ra = 5;
cout << " a的地址是:" << &a << ", a的值是:" << a << endl;
cout << "ra的地址是:" << &ra << ",ra的值是:" << ra << endl;
示例17输出结果
a的地址是:00000086A498F514, a的值是:3
ra的地址是:00000086A498F514,ra的值是:3
a的地址是:00000086A498F514, a的值是:5
ra的地址是:00000086A498F514,ra的值是:5
–
把函数的形参声明为引用,调用函数的时候,形参将成为实参的别名。这种方法也叫按引用传递或传引用。引用的本质是指针,传递的是变量的地址,在函数中,修改形参会影响实参。下面的例子给出了传值、传地址、传引用的示例:
示例18
#include <iostream>
using namespace std;
void fout(int num, string str) //传值
cout << num << " " << str << endl;
num = 66; str = "MY";
void fout2(int* num, string* str) //传地址
cout << *num << " " << *str << endl;
*num = 66; *str = "MY";
void fout3(int& num, string& str) //传引用
cout << num << " " << str << endl;
num = 66; str = "MY";
int main()
int no; string st;
no = 99; st = "慕羽";
fout(no, st);
cout << no << " " << st << endl;
cout << endl;
no = 99; st = "慕羽";
fout2(&no, &st);
cout << no << " " << st << endl;
cout << endl;
no = 99; st = "慕羽";
fout3(no, st);
cout << no << " " << st << endl;
示例18输出结果
99 慕羽
99 慕羽
99 慕羽
66 MY
99 慕羽
66 MY
–
传值、传地址、传引用的差异可概括成下表所示,相比之下,传引用更简洁有效,
《C++ Primer Plus》中给出了传值、传地址和传引用的指导原则
(1)如果不需要在函数中修改实参
如果实参很小,如C++内置的数据类型或小型结构体,则按值传递。
如果实参是数组,则使用const指针,因为这是唯一的选择(没有为数组建立引用的说法)。
如果实参是较大的结构,则使用const指针或const引用。
如果实参是类,则使用const引用,传递类的标准方式是按引用传递(类设计的语义经常要求使用引用)。
(2)如果需要在函数中修改实参
如果实参是内置数据类型,则使用指针。只要看到func(&x)的调用,表示函数将修改x。
如果实参是数组,则只能使用指针。
如果实参是结构体,则使用指针或引用。
如果实参是类,则使用引用。
当然,这只是一些指导原则,很可能有充分的理由做出其他的选择。
此外,传引用不必使用二级指针,如下面的例子所示:
示例19
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
void func1(int** p) // 传地址,实参是指针的地址,形参是二级指针。
*p = new int(3); // p是二级指针,存放指针的地址。
cout << "func1内存的地址是:" << *p << ",内存中的值是:" << **p << endl;
void func2(int*& p) // 传引用,实参是指针,形参是指针的别名。
p = new int(3); // p是指针的别名。
cout << "func2内存的地址是:" << p << ",内存中的值是:" << *p << endl;
int main()
int* p = nullptr; // 存放在子函数中动态分配内存的地址。
//func1(&p); // 传地址,实参填指针p的地址。
func2(p); // 传引用,实参填指针p。
cout << "main 内存的地址是:" << p << ",内存中的值是:" << *p << endl;
delete p;
示例19输出结果
func2内存的地址是:000001CB275010F0,内存中的值是:3
main 内存重学C++:笔记C++基础容器&C++指针引用
文章目录
-
第4章 C++基础容器
4.1 序列容器–数组
off-by-one error数组下标
off-by-one error (差一错误)
数组中通过左闭右开的方式可以避免上述错误,如:
for (int i = 0; i <10 ; i++)
cout << a[i] << endl;
数组设计的原则
从0开始,使用非对称空间:
让下界(左侧)可以取到值,让上界(右侧)取不到值;
好处:
- 取值范围的大小:上界-下界
- 如果取值范围为空,上界值==下界值
- 及时取值范围为空上界值也永远不可能小于下界值
4.2 数组的增删改查及二维数组
二维数组设计的tips(循环时尽可能要满足空间局限性)
- 在一个小的时间窗口内,访问的变量地址越接近越好,这样执行速度快
- 一般来说,需要将最长的循环放最内层,最短的循环放在最外层,以减少CPU跨层的次数
4.3 动态数组vector
使用前的准备(引入头文件和namespace)
#include <vector>
using namespace std;
相关方法
vector<int> vec = 1, 2, 3;
//在尾部插入元素
vec.push_back(4);
//在中间进行元素插入
vec.insert(vec.end()-1, 4); //在尾部前一个位置插入4
//删除尾部元素
vec.pop_back();
//删除中间元素
vec.erase(vec.end()-1); //删除尾部前一个位置的元素
//获取当前容量
vec.capacity();
//获取已经存储的元素个数
vec.size();
4.4 字符串
字符串变量
- 字符串是以空字符(’\\0’)结束的字符数组
- 空字符自动添加到字符串的内部表示中
- 在声明字符串变量时,应该位这个空结束符预留一个额外元素的空间,如:char s [11] = “helloworld” ;
Unicode编码
Unicode编码:最初的目的是把世界上的文字都映射到一套字符空间中
- UTF-8
- UTF-16
- UTF-16BE
- UTF-16LE
- UTF-32
- UTF-32BE
- UTF-32LE
编码错误的根本原因在于编码方式和解码方式的不统一
Windows的文件可能有BOM(byte order mark),如果要在其他平台使用,可以去掉BOM
字符串的指针表示
指针所指的区域能否改变取决于指针指向的那块区域是否为可变的,如若指向的为常量,则不可变,而若指向的区域为变量,那么可以改变
字符串数组数组名定义了之后就不可变,但是数组里的值可变,而定义一个指针,指针变量的值是可变的,但是若其指向的为常量,那么则指针指向的字符串的值不可变
字符串的基本操作
包含在头文件<string.h>中
-
字符串长度:strlen(s)
返回字符串s的长度(s的长度不包括 ’ \\0 ')
区别sizeof(),sizeof()计算的为占用的空间,strlen()计算的为字符串长度
-
字符串比较:strcmp(s1, s2)
若s1和s2 是相同的,则返回0;
若s1 < s2 则返回值小于0;
若s1 > s2 则返回值大于0
-
字符串拷贝:strcpy(s1, s2)
复制字符串s2到字符串s1
-
复制指定长度字符串:strncpy(s1, s2, n)
将字符串s2中前n个字符串拷贝到s1中
-
字符串拼接:strcat(s1, s2)
将字符串s2拼接到s1之后
-
查找字符串:strchr(s1, ch)
指向字符串s1中字符ch的第一次出现的位置
-
查找字符串:strstr(s1, s2)
指向字符串s1中字符串s2的第一次出现的位置
进行底层操作时,为避免编译器错误发出报错,可添加宏:_CRT_SECURE_NO_WARNINGS
缓冲区溢出问题
举例:在进行字符串拼接的操作时,若拼接长度过大而超出原本的长度,那么就有可能把存储区原本存储的信息改变,造成逻辑的改变
解决方法:养成进行边界判断的习惯,另外也存在更安全的API可供调用,如strcpy()更为安全的版本为strcpy_s(),其他几个字符串操作函数的安全版本均为*_s(),例如strcat_s(s1, size, s2,),当s2的长度大于size时,那么会报错,无法运行
string简介
#include<string>
using namespace std;
//定义字符串变量
string s;//定义空字符串
string s = "helloworld";//定义并初始化
string s ( "helloworld" );
string s = string( "helloworld" );
字符串相关函数:
-
获取字符串的长度
s.length()//字符串长度
s.size()//同上
s.capacity()//字符串s所占空间大小
-
字符串比较:= = ! = > > = < < =
string s1 = "hello", s2 = "world";
cout << (s1 == s2) << endl;//返回0
cout << (s1 != s2) << endl;//返回1
字符串的常用操作:
//转换为C风格的字符串
const char *c_str1 = s1.c_str();
//随机访问(获取字符串中某个字符):[]
string s = "hello";
cout << s[0] << endl;
//字符串拷贝:=
string s1 = "hello";
string s2 = s1;
//字符串拷贝:+、+=
string s1 = "hello", s2 = "world";
string s3 = s1 + s2;//s3:helloworld
s1 += s2;//s1:helloworld
总结:string结合了C++的新特性,使用起来比原始的C风格更安全和方便,对性能要求不是特别高的常见情况可以使用
第5章 彻底学会 C++ 指针,引用
5.1 指针的概念
C++中内存单元内容与地址
指针本身就是一个变量,其符合变量定义的基本形式,它存储的是值的地址。对类型T,T*是“到T的指针”类型,一个类型为T*的变量能保存一个类型T的对象的地址
通过一个指针访问它所指向的地址的过程称为间接访问或者引用指针,这个用于执行简介访问的操作符是单目运算符*
一个变量有三个信息:
- 变量的地址信息
- 变量所存的信息
- 变量的类型
左值与右值
左值:编译器为其单独分配了一块存储空间,可以取其地址的,左值可以放在赋值运算符的左边(最常见的情况如函数和数据成员的名字)
右值:指数据本身,不能取到其自身地址,右值只能在赋值运算符右边(右值是没有标识符,不可以取地址的表达式,一般也称之为临时对象)
一般指针数组和指针数组
指针的数组与数组的指针:
- 指针的数组(定义的是一个包含n个元素数组,数组里存放的都是T类型的指针):T* t[n]
- 数组的指针(定义了一个指针,指针指向一个包含n个T类型元素的数组):T(*t) [n](注意[]的优先级比较高)
访问数组的指针所指向的数组的元素时,如:(*t)[3]表示指针所指向的数组的下标为3的元素
const与指针
char const *pStr1 = "helloworld";
char* const pStr2 = "helloworld";//pStr2不可改
char const* const pStr3 = "helloworld";//pStr3不可改
关于const修饰符的部分:
- 看左侧最近的部分
- 如果左侧没有,则看右侧
如pStr1指针所指向的地址的内容不能变,pStr2指针指向的地址不能变,而pStr3二者均不可改变
指向指针的指针
int a = 123;
int* b = &a;
int** c = &b;//**c相当于*b,即得到a的值(表达式从里向外逐层求值)
//*操作符具有从右向左的结合性
野指针
-
未初始化和非法指针
//eg:
int* a;//仅定义了一个指针,其所指的区域不确定
*a = 12;
- 定位到一个非法地址,从而终止
- 定位到一个可以访问的地址,无意修改了它,这样的错误难以捕捉引发的错误可能与原先用于操作的代码完全不相干
避免方法:用指针进行间接访之前,一定要确保其已经初始化,并被恰当地赋值
-
NULL指针
一个特殊的指针变量,表示不指向任何东西
int* a = NULL;
NULL指针给出一种方法,来表示特定的指针目前未指向任何东西
注意事项:
- 对于一个指针,若已经知道将被初始化为什么地址,那么赋给它这个地址值,否则将其设置为NULL
- 在对任何一个指针进行间接引用前,先判断这个指针是否为空
-
杜绝“野”指针
指向“垃圾”内存的指针,if等判断对它们不起作用,因为没有置NULL
三种情况:
- 指针变量没有初始化
- 已经释放不用的指针没有置NULL,如delete和free之后的指针
- 指针操作超越了变量的作用范围
注意事项:没有初始化的,不用的或者超出范围的指针将其值置为NULL
5.2 指针的基本操作
&与*操作符
指针类型默认都是四个字节大小,而与其指向的空间的数据类型无关
char ch = 'a';
char* cp = &ch;
//表达式
//*cp
char ch2 = *cp;//作为右值,将a赋给ch2
*cp = 'b';//作为左值,等价于对ch进行操作
//*cp + 1
char ch2 = *cp + 1;//作为右值,将b赋给ch2
*cp + 1 = ...;//作为左值非法
//*(cp + 1)
char ch2 = *(cp + 1);//作为右值,将ch后一个地址的元素的值赋给ch2
*(cp + 1) = 'b';//作为左值,等价于对ch后一个地址的元素进行操作
原始指针的基本运算
++ 与 – 操作符
char* cp2 = ++cp;
/*
mov eax,dword ptr [cp]
add eax,1
mov dword ptr [cp],eax
mov ecx,dword ptr [cp]
mov dword ptr [cp2],ecx
*/
char* cp3 = cp++;
/*
mov eax,dword ptr [cp]
mov dword ptr [cp3],eax
mov exc,dword ptr [cp]
add ecx,1
mov dword ptr [cp],ecx
*/
// -- 操作同理 add --> sub
*++p = a;
//相当于对p + 1所指的位置进行赋值操作
*p++ = a;
//相当于对p所指位置进行赋值后p的
关于++++,----等运算符
编译器分解程序符号的方法:逐字符读入,若该字符可能组成一个符号,那么读入下一个字符,知道读入的字符不能在组成一个有意义的符号(贪心法)
int a = 1, b = 2, c, d;
c = a +++ b; // --> (a ++) + b
d = a ++++ b; //error
++*++p; // --> ++(*(++p))
5.3 CPP程序的存储区域划分
#include "stdafx.h"
#include <string>
int a = 0; //(GVAR)全局初始化区
int* p1; //(bss)全局未初始化区
int main() //(text)代码区
int b=1; //(stack)栈区变量
char s[] = "abc"; //(stack)栈区变量
int*p2=NULL; //(stack)栈区变量
char *p3 = "123456"; //123456\\0在常量区, p3在(stack)栈区
static int c = 0; //(GVAR)全局(静态)初始化区
p1 = new int(10); //(heap)堆区变量
p2 = new int(20); //(heap)堆区变量
char* p4 = new char[7]; //(heap)堆区变量
strcpy_s(p4, 7, "123456"); //(text)代码区
//(text)代码区
if (p1 != NULL)
delete p1;
p1 = NULL;
if (p2 != NULL)
delete p2;
p2 = NULL;
if (p4 != NULL)
delete[ ] p4;
p4 = NULL;
//(text)代码区
return 0; //(text)代码区
- main函数内定义的变量存储在栈区,且后定义的地址更低;
- new所申请的区域为堆区,后申请的地址更高;
- 全局变量和static修饰的变量存在全局区
- 指针指向内容是否可以改变取决于所指向的区域,常量区的值不可改变,栈区可以改变;
- 其他代码一般存在代码区;
5.4 CPP动态分配和回收原则
动态分配资源:堆(heap)
程序通常需要涉及三个内存管理器的操作:
- 分配一个某个大小的内存块;
- 释放一个之前分配的内存块;
- 垃圾收集操作。寻找不再使用的内存块并予以释放;
回收策略需要实现性能、实时性、额外开销等方面的平衡,很难有统一和高效的做法。
C++做了1,2;而Java做了1,3。
5.5 RAII初步
资源管理方案:RAII(Resource Acquisition Is Initialization)
RAII依托栈和析构函数,来对所有的资源:包括堆内存在内进行管理。对RAII的使用,使得C++不需要类似于Java那样的垃圾收集方法,也能有效地对内存进行管理。RAII 的存在,也是垃圾收集虽然理论上可以在C++使用,但从来没有真正流行过的主要原因。
RAII比较成熟的智能指针代表:
std::auto_ptr
boost::shared_ptr
5.6 C++中几种变量的对比
栈和堆中的变量对比
全局静态存储区和常量存储区的变量对比
5.7 内存泄漏
什么是内存泄漏问题(memory leak)
指程序中己动态分配的堆内存由于某种原因程序未释放或无法释,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
内存泄漏发生原因和排查方式
- 内存泄漏主要发生在堆内存分配方式中,即“配置了内存后,所有指
向该内存的指针都遗失了”。若缺乏语言这样的垃圾回收机制,这样的内存
片就无法归还系统。 - 因为内存泄漏属于程序运行中的问题,无法通过编译识别,所以
只能在程序运行过程中来判别和诊断。
注意delete 和delete[] 的区别。
5.8 智能指针
使用指针是非常危险的行为,可能存在空指针,野指针问题,并可能造成内存泄漏问题。
可指针又非常的高效,所以我们希望以更安全的方式来使用指针。
两种方案:
- 使用更安全的指针:智能指针
- 不使用指针,使用更安全的方式:引用
C+ +中推出了四种常用的智能指针:
unique_ ptr、 shared ptr、weak_ ptr 和C++ 11中已经废弃(deprecated)的auto_ ptr,在C++ 17中被正式删除。
auto_ptr
由new expression获得对象,在auto_ ptr 对象销毁时,他所管理的对象也会
自动被delete掉。
所有权转移:不小心把它传递给另外的智能指针,原来的指针就不再拥有这个对象了。在拷贝/赋值过程中,会直接剥夺指针对原对象对内存的控制权,转交给新对象,然后再将原对象指针置为nullptr。
演示代码:
#include <string>
#include <iostream>
#include <memory>
using namespace std;
int main()
// 确定auto_ptr失效的范围
// 对int使用
auto_ptr<int> pI(new int(10));
cout << *pI << endl; // 10
// auto_ptr C++ 17中移除 拥有严格对象所有权语义的智能指针
// auto_ptr原理:在拷贝 / 赋值过程中,直接剥夺原对象对内存的控制权,转交给新对象,
// 然后再将原对象指针置为nullptr(早期:NULL)。这种做法也叫管理权转移。
// 他的缺点不言而喻,当我们再次去访问原对象时,程序就会报错,所以auto_ptr可以说实现的不好,
// 很多企业在其库内也是要求不准使用auto_ptr。
auto_ptr<string> languages[5] =
auto_ptr<string>(new string("C")),
auto_ptr<string>(new string("Java")),
auto_ptr<string>(new string("C++")),
auto_ptr<string>(new string("Python")),
auto_ptr<string>(new string("Rust"))
;
cout << "There are some computer languages here first time: \\n";
for (int i = 0; i < 5; ++i)
cout << *languages[i] << endl;
auto_ptr<string> pC;
pC = languages[2]; // languges[2] loses ownership. 将所有权从languges[2]转让给pC,
//此时languges[2]不再引用该字符串从而变成空指针
cout << "There are some computer languages here second time: \\n";
for (int i = 0; i < 2; ++i)
cout << *languages[i] << endl;
cout << "The winner is " << *pC << endl;
//cout << "There are some computer languages here third time: \\n";
//for (int i = 0; i < 5; ++i)
//
// cout << *languages[i] << endl;
//
return 0;
unique_ptr
unique_ptr是专属所有权,所以unique_ptr管理的内存,只能被一个对象持有,不支持复制和赋值。
移动语义: unique_ptr禁止了拷贝语义,但有时我们也需要能够转移所有权,于是提供了移动语义,即可以使用std::move()进行控制所有权的转移。
#include <memory>
#include <iostream>
using namespace std;
int main()
// 在这个范围之外,unique_ptr被释放
auto i = unique_ptr<int>(new int(10));
cout << *i << endl;
// unique_ptr
auto w = std::make_unique<int>(10);
cout << *(w.get()) << endl; // 10
//auto w2 = w; // 编译错误如果想要把 w 复制给 w2, 是不可以的。
// 因为复制从语义上来说,两个对象将共享同一块内存。
// unique_ptr 只支持移动语义, 即如下
auto w2 = std::move(w); // w2 获得内存所有权,w 此时等于 nullptr
cout << ((w.get() != nullptr) ? (*w.get()) : -1) << endl; // -1
cout << ((w2.get() != nullptr) ? (*w2.get()) : -1) << endl; // 10
return 0;
shared_ptr
shared_ptr通过一个引用计数共享一个对象.
shared_ ptr 是为了解决auto_ ptr在对象所有权上的局限性,在使用引用计数的机制上提供了可以共享所有权的智能指针,当然这需要额外的开销。
通过引用计数对引用个数进行记录,当引用计数为0时,该对象没有被使用,可以进行析构。
#include <iostream>
#include <memory>
using namespace std;
int main()
// shared_ptr
//shared_ptr 代表的是共享所有权,即多个 shared_ptr 可以共享同一块内存。
auto wA = shared_ptr<int>(new int(20));
auto wA2 = wA;
cout << ((wA2.get() != nullptr) ? (*wA2.get()) : -1) << endl; // 20
cout << ((wA.get() != nullptr) ? (*wA.get()) : -1) << endl; // 20
cout << wA2.use_count() << endl; // 2
cout << wA.use_count() << endl; // 2
cout << wA2.use_count() << endl;
cout << wA.use_count() << endl; // 1
cout << ((wA.get() != nullptr) ? (*wA.get()) : -1) << endl; // 20
//shared_ptr 内部是利用引用计数来实现内存的自动管理,每当复制一个 shared_ptr,
// 引用计数会 + 1。当一个 shared_ptr 离开作用域时,引用计数会 - 1。
// 当引用计数为 0 的时候,则 delete 内存。
// move 语法
auto wAA = std::make_shared<int>(30);
auto wAA2 = std::move(wAA); // 此时 wAA 等于 nullptr,wAA2.use_count() 等于 1
cout << ((wAA.get() != nullptr) ? (*wAA.get()) : -1) << endl; // -1
cout << ((wAA2.get() != nullptr) ? (*wAA2.get()) : -1) << endl; // 30
cout << wAA.use_count() << endl; // 0
cout << wAA2.use_count() << endl; // 1
//将 wAA 对象 move 给 wAA2,意味着 wAA 放弃了对内存的所有权和管理,此时 wAA对象等于 nullptr。
//而 wAA2 获得了对象所有权,但因为此时 wAA 已不再持有对象,因此 wAA2 的引用计数为 1。
return 0;
带来的问题:循环引用
循环引用会导致堆里的内存无法正常回收,造成内存泄漏。
weak_ptr
weak_ ptr 被设计为与shared_ ptr 共同工作,用- -种观察者模式工作。
作用是协助shared_ptr 工作,可获得资源的观测权,像旁观者那样观测资源的使用情况。
观察者意味着weak_ ptr只对shared_ ptr 进行引用,而不改变其引用计数,当被观察的shared_ ptr 失效后,相应的weak_ ptr 也相应失效。
#include <string>
#include <iostream>
#include <memory>
using namespace std;
struct B;
struct A
shared_ptr<B> pb;
~A()
cout << "~A()" << endl;
;
struct B
shared_ptr<A> pa;
~B()
cout << "~B()" << endl;
;
// pa 和 pb 存在着循环引用,根据 shared_ptr 引用计数的原理,pa 和 pb 都无法被正常的释放。
// weak_ptr 是为了解决 shared_ptr 双向引用的问题。
struct BW;
struct AW
shared_ptr<BW> pb;
~AW()
cout << "~AW()" << endl;
;
struct BW
weak_ptr<AW> pa;
~BW()
cout << "~BW()" << endl;
;
void Test()
cout << "Test shared_ptr and shared_ptr: " << endl;
shared_ptr<A> tA(new A()); // 1
shared_ptr<B> tB(new B()); // 1
cout << tA.use_count() << endl;
cout << tB.use_count() << endl;
tA->pb = tB;
tB->pa = tA;
cout << tA.use_count() << endl; // 2
cout << tB.use_count() << endl; // 2
void Test2()
cout << "Test weak_ptr and shared_ptr: " << endl;
shared_ptr<AW> tA(new AW());
shared_ptr<BW> tB(new BW());
cout << tA.use_count() << endl; // 1
cout << tB.use_count() << endl; // 1
tA->pb = tB;
tB->pa = tA;
cout << tA.use_count() << endl; // 1
cout << tB.use_count() << endl; // 2
int main()
Test();
Test2();
return 0;
5.9 引用
引用:一种特殊的指针,不允许修改的指针。
使用指针有哪些坑:
- 空指针;
- 野指针;
- 不知不觉改变了指针的值,却继续使用;
使用引用,则可以:
- 不存在空引用;
- 必须初始化;
- 一个引用永远指向它初始化的那个对象;
引用的基本使用:可以认为是指定变量的别名,使用时可以认为是变量本身
示例:
int x = 1, x2 = 3;
int& rx = x;
rx = 2;
cout << x << endl; // 2
cout << rx << endl; // 2
rX = x2;
cout << x << endl; // 3
cout << rx << endl; // 3
两个问题
有了指针为什么还需要引用?
Bjarne Stroustrup的解释:为了支持函数运算符重载;
有了引用为什么还需要指针?
Bjarne Stroustrup的解释:为了兼容C语言。
补充:关于函数传递参数类型的说明
-
对内置基础类型(如int,double等)而言:
在函数中传递时pass by value更高效;
-
对OO面向对象中自定义类型而言:
在函数中传递时pass by reference to const更高效。
以上是关于C++温习笔记(慕羽★)——指针及相关内容(下)的主要内容,如果未能解决你的问题,请参考以下文章