1 指针数组
指针数组的每个元素都是地址
int *p[2];由p[0] 和p[1]两个指针组成
指针数组和二维数组区别
- 当每行的元素个数是一样的时候,两者是一样的
- 当每行的元素个数不同,只有指针数组才能实现
- 二维数组每行的列数是一样的,指针数组每行的列数没有要求
int line1[]={1,0,0};
int line2[]={1,0};
int *pline[2]={line1,line2};
-
元素访问
指针数组的地址不是连续的,每行内元素的地址连续,但行与行不连续,在使用指针访问时,要特别注意
image.png
2 指针作为函数参数
为什么需要指针做参数?
- 需要数据的双向传递时(引用也可以达到此效果)。
双向传递:将主调函数中已经定义好的变量地址放在指针中传给被调函数,在被调用的函数体中,可以通过被传过来的指针直接对主调函数中的变量进行操作 - 需要传递一组数组,只传递首地址效率比较高。
3 指向常量的指针做形参
const int N=6;
void print(const int *p,int n);
指向常量的指针做形参:双向传递变成了单向,通过指针可以读取指向的对象,但不能修改对象的值。
最小的授权原则
不要过多的授权
4 指针类型的函数
指针类型的函数:函数的返回值是指针
指针类型的函数定义形式
存储类型 数据类型 *函数名{
//函数体语句
}
注意事项
- 不要将非静态的局部地址用作函数的返回值。
在函数内部声明一个局部变量,函数返回这个局部变量的地址返回给主函数,就是非法地址
image.png - 在子函数中通过动态内存分配new操作取得的内存地址返回给主函数是合法有效的,但是内存分配和释放不在同一级别,要注意不能忘记释放,避免内存泄漏。
image.png
5 new在局部中申请的内存空间,在离开局部后不会被释放,即离开包围函数体的大括号不会被释放,要对应用delete函数释放!!!
6 指向函数的指针/函数指针
- 定义形式
存储类型 数据类型 (*函数指针名)();
指针在初始化时要声明指向的类型,在函数指针中, 初始化时要声明返回值和形参类型 - 含义
函数指针指向的是程序代码存储区
为什么需要指向函数的指针?函数指针的用途
函数指针的经典用途 ——实现函数回调
- 通过函数指针调用的函数
例如将函数的指针作为参数传递给一个函数,使得在处理相似事件的时候可以灵活使用不同的方法 - 调用者不关心谁是被调用者
需知道存在一个具有特定原型和限制条件的被调函数(知道知道这个函数的参数表和返回值类型)
函数原型则特指包括说明参数类型的函数声明。
函数指针举例
#include<iostream>
using namespace std;
int compute(int a, int b, int (*func)(int,int) ){
//获得了func函数的地址,就可以访问这个函数
return func(a ,b);
}
int max(int a,int b)
{
return (a>b)?a:b;
}
int min(int a,int b)
{
return (a<b)?a:b;
}
int add(int a,int b)
{
return a+b;
}
int main(){
int a,b,res;
cin>>a>>b;
res=compute(a,b,&max);
//res=compute(a,b,max);不加&也可以,函数名本身就代表函数代码的起始地址,加&只是更清晰
return 0;
}
7数组名与数组名前加取地址符
对数组int a[10]来讲,a和&a都表示地址,准确来讲它们不是地址,而是指针(地址基本上算是错误的说法)!
虽然,指针的内容即指向的地址相同,但指针类型不同:
a是指向数组元素int的指针int,指针操作的基本单位为4Bytes;
而&a是指向数组a[10]的指针int()[10],指针操作的基本单位为40Bytes。
指针与取指针变量的地址
假设0x80000地址的值5,int p ,p这个变量在内存中的地址为0x8abcd,
那么 p = 0x80000;p = 5; &p =0x8abcd;
p是变量i的地址,相当于一维指针
&p是变量p的地址,相当于一个二维指针
对函数指针来说,用指针名和&指针名代表的地址都是一样的
8 区分函数指针和指针函数
定义不同
指针函数本质是一个函数,其返回值为指针。
函数指针本质是一个指针,其指向一个函数。
写法不同
指针函数:int* fun(int x,int y);
函数指针:int (*fun)(int x,int y);
9 对象指针
对象指针:指向对象的指针
- 形式
类型 *对象指针
例:
Point a(5,10);
Point *ptr;
ptr=&a;
- 访问对象的元素
对象指针名->成员名 ==(*对象指针名).成员名
对象指针与前向引用结合
class Fred;
class Barney {
Fred x;//错误,在给出Fred的具体细节之前不能使用Fred来声明一个对象
};
class Fred {
Barney y;
};
class Fred;
class Barney {
Fred *x;//正确,只是声明了一个指向Fred类的指针,不需要知道Fred类的具体细节
};
class Fred {
Barney y;
};
10 this 指针
问题描述:
当前类中有多个对象,当某个对象调用getX()函数时,getX()要返回哪个对象的成员x给当前对象?
解决:
this指针:指向当前对象自己的指针
- 隐含与类的每一个非静态成员函数中
- 指出成员函数所操作的对象
当通过一个对象调用成员函数时,系统先将该对象的地址赋给this指针,然后调用成员函数,成员函数对对象的数据成员进行操作时,就隐含使用了this指针 - 例如 Point类的getX函数中的语句:
return x;
相当于
return this->x;
11 动态内存分配
当必须用动态内存分配的方式去申请内存单元时,那么动态申请的单元没有机会给它起变量名,返回一个首地址
动态申请内存操作符 new
new 类型名T(初始化参数列表)//给一个对象申请空间,并在()内初始化,其中()可以省略
new 类型名T [数组个数]//申请一个容纳多个对象的内存空间
- 功能:
在程序执行期间,申请用于存放T类型对象的内存空间,并依初值列表赋以初值 - 结果值:
成功:返回T类型的指针,指向的是新分配内存空间的起始地址
失败:抛出异常
释放内存操作符delete
delete 指针p
- 功能:释放指针p所指向的内存。p必须是new操作的返回值
- 注意,new和delete必须配套使用,delete只能释放new申请的空间,不能释放用其他方式(比如malloc)申请的空间
#include<iostream>
using namespace std;
class Point {
public:
Point() :x(0), y(0) {
cout << "Default Constructor called." << endl;
}
Point(int x, int y) :x(x), y(y) {
cout << "Constructor called." << endl;
}
~Point() { cout << "Destructor called." << endl; }
int getX() { return x; }
int getY() { return y; }
void move(int newX, int newY) {
x = newX;
y = newY;
}
private:
int x, y;
};
int main() {
cout << "Step one:" << endl;
Point* ptr1 = new Point;//没有初始化,调用的是默认构造函数
delete ptr1;//删除对象,自动调用析构函数,删除的是指向的对象不是指针,指针在main函数结束后自动被释放
cout << "Step two:" << endl;
ptr1 = new Point(1,2);//有初始化,调用的是构造函数
delete ptr1;
return 0;
}
结果
Step one:
Default Constructor called.
Destructor called.
Step two:
Constructor called.
Destructor called.
申请和释放动态数组
- 分配: new 类型名T[数组长度]
- 数组长度可以是任何整数类型表达式,在运行时计算
- 释放:delete[] 数组名p
- 释放指针p所指向的数组,必须加方括号[],不加方括号仅仅会释放数组首元素的地址,p必须使用new分配得到的数组首地址
#include<iostream>
using namespace std;
class Point {
public:
Point() :x(0), y(0) {
cout << "Default Constructor called." << endl;
}
Point(int x, int y) :x(x), y(y) {
cout << "Constructor called." << endl;
}
~Point() { cout << "Destructor called." << endl; }
int getX() { return x; }
int getY() { return y; }
void move(int newX, int newY) {
x = newX;
y = newY;
}
private:
int x, y;
};
int main() {
cout << "Step one:" << endl;
Point* ptr = new Point[2];//创建对象数组,调用的都是默认构造函数
ptr[0].move(5,10);//通过指针访问数组元素的成员
ptr[1].move(5,20);
cout << "Deleting " << endl;
delete[] ptr;
return 0;
}
结果
Step one:
Default Constructor called.
Default Constructor called.
Deleting
Destructor called.
Destructor called.
int a[10]和int *p=new int[10]区别
- 在函数中 创建int a[10],不能返回这个数组的首地址,因为它是一个局部变量,在函数执行完后,内存就会自动被释放
- 在函数中 创建int *p=new int[10] 可以返回p,因为new开辟的空间只有delete才会释放,系统不会自动释放
动态创建多维数组(行不定,列确定)用数组指针,数组元素的地址是连续的
new 类型名T[第一维长度][第二维长度]
- 如果内存申请成功,new运算返回一个指向新分配内存首地址的指针(地址是连续的)。
- 这种申请方式下,第二维长度必须是一个常量,不能是变量
-
例如
char (*fp)[3];//数组指针,指向的是一个字符数组(整个数组 ),3为数组的长度,当fp+1时需要跨越3个字符数据的长度
fp=new char[2][3];
多维数组的行和列数都不确定 用指针数组,数组元素的地址不连续的
创建
int c=8;//列
int r=7;//行
int **p=new int*[r];//第一次动态内存分配,生成的是一个指针数组,数组的元素都是指针,指向的是一个一维数组的首地址
for(int i=0;i<r;i++){
p[i]=new int[c];//第二次内存分配,依次生成r个一位数组, p[i]指向的是第i行的首地址
//二维数组p的地址不是连续的,不能用p++来遍历所有元素
}
释放
for(int i=0;i<r;i++){
delete[] p[i];//加[]是因为p[i]也是一个数组,p[i]指向的是数组的首地址
}
delete [] p;
创建三维数组
创建动态多维数组总结
创建行不定 列确定和行不定 列也不确定的二维数组其实本质上没什么区别:申请空间时,必须要确定申请空间的最小单元。
- 行不定,列确定。
//最小单元是一个固定长度一维数组
//每次申请一个确定的一维数组int[常量]大小的空间,根据需求动态申请多个这样的连续空间
int (*p)[常量]=new int[变量][常量]
- 行不定,列不定。
//最小单元是一个指针
//每一次申请一个指针大小的空间,根据需求申请多个这样连续的空间,即动态指针数组
int **p=new int *[变量1]
//然后再给动态指针数组的每个元素所指针的地址动态申请空间
for(int i=0;i<变量1;i++){
p[i]=new int[变量2];
}
//这样数组p[变量1][变量2]就创建好了
12 将动态数组封装成类(难点,多看看)
好处:
- 更加简洁,便于管理
- 可以在访问数组元素前检查是否越界
#include<iostream>
#include <cassert>
using namespace std;
class Point {
public:
Point() :x(0), y(0) {
cout << "Default Constructor called." << endl;
}
Point(int x, int y) :x(x), y(y) {
cout << "Constructor called." << endl;
}
~Point() { cout << "Destructor called." << endl; }
int getX() { return x; }
int getY() { return y; }
void move(int newX, int newY) {
x = newX;
y = newY;
}
private:
int x, y;
};
class ArrayOfPoints {//动态数组类
public:
ArrayOfPoints(int size) :size(size) {
points = new Point[size];
}
~ArrayOfPoints() {
delete[] points;
}
//返回数组中下标为index的元素,返回值类型是引用,之所以用引用是想在后边改变返回值数组元素
Point& element(int index) {
assert(index >= 0 && index < size);//断言,如果它的条件返回错误,注意是错误,则终止程序执行
return points[index];//返回数组中下标为index的元素
}
private:
Point* points;
int size;
};
int main() {
int count;
cout << "Please enter the count of points:" << endl;
cin >> count;
ArrayOfPoints poinst(count);
poinst.element(0).move(5, 0);
poinst.element(1).move(15, 20);
return 0;
}
13 智能指针
C++11提供了一种智能指针,它提供了一种垃圾回收制度
智能指针有三种
- unique_ptr:
不允许多个指针共享资源,这个指针指向的空间只能由它自己所指向,不允许其它指针指向这个地址,可以用标准库中的move函数转移指针,把这个指针中的地址转移到别的指针中,原指针失效 - shared_ptr
多个指针共享资源,多个share_ptr指针可以指向同一个内存单元 - weak_ptr
可复制share_ptr指针所指向的单元,但其构造或释放对资源不产生影响
14 vector类(是一个类)
vector对象是C++标准库里面的一个类模板
为什么需要vector?
- vector类封装成任何类型的动态数组,自动创建和删除,不需要像前面第12小节一样要对不同的类型封装成不同的类,前面来一个新的类型,就要写一个新的类来封装它
- 数组下标越界检查
- 需要注意的是vector运行效率比较低,在刷一些算法题时最好不好用vector
vector对象的定义
vector<数据类型>数组对象名(数组长度);
例如:
vector<int >arr(5);
vector对象的访问
- 对数组元素的引用
对象数组名[下标]
arr[2];
- 获得数组长度(vector类中的成员函数size())
对象数组名.size()
vector类作为函数参数
- 值传递,在函数中不能改变a的值,会发生拷贝构造
//声明:
void fun1(vector<int>vec);
//调用
fun1(vec);
- 地址传递,在函数中可以改变a的值,不会发生拷贝构造
//引用传递,不会发生拷贝构造
void fun2(vector<int>&vec);
fun2(vec);
//指针传递
void fun2(vector<int>*vec);
fun2(&vec);
代码示例
v.begin();是一个迭代器
auto是自动类型
更多vector成员函数的介绍
更多vector类的成员函数介绍可以借鉴下面这篇博客
https://blog.csdn.net/weixin_41743247/article/details/90635931
15 深层复制与浅层复制
浅层复制
- 实现对象间数据元素的一一对应复制
- 对于指针,是将指针中存放的地址复制过去
//浅层复制
int *i=new int(10);
int *j=i;//i和j指向的是同一块内存
- 默认复制构造函数实现的是浅层复制
深层复制
- 当被复制的对象数据成员时指针类型时(不管是指针数组还是单一指针都要进行深层复制),不是复制该指针成员本身,而是将该指针所指对象进行复制
- 深层复制会新开辟一个空间,这个空间存放的就是要被复制的对象,再用指针指向这个空间
- 深层复制就是给指针对象开辟一个新的空间,用来存放这个指针对象的拷贝
//深层复制
int *i=new int(2);//动态内存分配 new 数据类型(初始化列表)
int *j=new int(*i);//j和i指向对象的值相同,但指向的不是同一块内存
代码示例
#include<iostream>
#include <cassert>
using namespace std;
class Point {
public:
Point() :x(0), y(0) {
cout << "Default Constructor called." << endl;
}
Point(int x, int y) :x(x), y(y) {
cout << "Constructor called." << endl;
}
~Point() { cout << "Destructor called." << endl; }
int getX() { return x; }
int getY() { return y; }
void move(int newX, int newY) {
x = newX;
y = newY;
}
private:
int x, y;
};
class ArrayOfPoints {//动态数组类
public:
ArrayOfPoints(int size) :size(size) {
points = new Point[size];
}
~ArrayOfPoints() {
delete[] points;
}
//返回数组中下标为index的元素,返回值类型是引用,之所以用引用是想在后边改变返回值数组元素
Point& element(int index) {
assert(index >= 0 && index < size);//断言,如果它的条件返回错误,注意是错误,则终止程序执行
return points[index];//返回数组中下标为index的元素
}
private:
Point* points;
int size;
};
int main() {
int count;
cout << "Please enter the count of points:" << endl;
cin >> count;
ArrayOfPoints poinst1(count);
poinst1.element(0).move(5, 0);
poinst1.element(1).move(15, 20);
ArrayOfPoints poinst2(poinst1);//用默认构造函数创建副本
cout << "copy of poinst1" << endl;
cout << poinst2.element(0).getX() << "," << poinst2.element(0).getY() << endl;
cout << poinst2.element(1).getX() << "," << poinst2.element(1).getY() << endl;
return 0;
}
结果
错误分析
修改后
#include<iostream>
#include <cassert>
using namespace std;
class Point {
public:
Point() :x(0), y(0) {
cout << "Default Constructor called." << endl;
}
Point(int x, int y) :x(x), y(y) {
cout << "Constructor called." << endl;
}
~Point() { cout << "Destructor called." << endl; }
int getX() { return x; }
int getY() { return y; }
void move(int newX, int newY) {
x = newX;
y = newY;
}
private:
int x, y;
};
class ArrayOfPoints {//动态数组类
public:
ArrayOfPoints(int size) :size(size) {
points = new Point[size];
}
~ArrayOfPoints() {
delete[] points;
}
ArrayOfPoints(const ArrayOfPoints& v) {
size = v.size;
//复制时,开辟新的空间,将参数数组的值一一复制给当前新数组
points = new Point[size];
for (int i = 0; i < size; i++) {
points[i] = v.points[i];
}
}
//返回数组中下标为index的元素,返回值类型是引用,之所以用引用是想在后边改变返回值数组元素
Point& element(int index) {
assert(index >= 0 && index < size);//断言,如果它的条件返回错误,注意是错误,则终止程序执行
return points[index];//返回数组中下标为index的元素
}
private:
Point* points;
int size;
};
int main() {
int count;
cout << "Please enter the count of points:" << endl;
cin >> count;
ArrayOfPoints poinst1(count);
poinst1.element(0).move(5, 0);
poinst1.element(1).move(15, 20);
ArrayOfPoints poinst2(poinst1);//用默认构造函数创建副本
cout << "copy of poinst1" << endl;
cout << poinst2.element(0).getX() << "," << poinst2.element(0).getY() << endl;
cout << poinst2.element(1).getX() << "," << poinst2.element(1).getY() << endl;
return 0;
}
16 移动构造
- C++11标准提供了一种新的构造方法——移动构造
- C++11之前,如果想要将源对象的状态转移到目标对象只能通过复制。在某些情况下,我们没有必要复制对象——只需要移动他们
- C++11引入移动语义:源对象资源的控制权全部交给目标对象
image.png
什么时候需要移动构造?
如果这个对象即将消亡,并且它的资源需要被再利用
移动构造函数
类名(类名&&)
(普通的复制构造 类名(const 类名&))
代码示例
#include<iostream>
using namespace std;
class IntNum {
public:
IntNum(int x = 1) :xptr(new int(x)) {
cout << "Calling constructor ..." << endl;
}
//在初始化列表中赋值别用等号 用()
//类里面只有数据成员会占用空间,成员函数不占用空间(成员函数放在了代码空间)
IntNum(const IntNum& n) :xptr(new int(*n.xptr)) {
cout << "Calling copy constructor ..." << endl;
}
~IntNum() {
delete xptr;
cout << "Destructing ..." << endl;
}
int getInt() { return *xptr; }
private:
int* xptr;
};
IntNum getNum() {
//调用的是默认构造函数
IntNum a;
//局部变量a在函数运行结束后就会消亡,return时会调用复制构造函数,产生一个临时无名对象,这个临时无名对象时局部变量a的一个备份
return a;
}
int main() {
cout << getNum().getInt() << endl;
return 0;
}
结果
Calling constructor ...
Calling copy constructor ...
Destructing ...
1
Destructing ...
移动构造
#include<iostream>
using namespace std;
class IntNum {
public:
IntNum(int x = 1) :xptr(new int(x)) {
cout << "Calling constructor ..." << endl;
}
//(不能加const修饰,因为要改变变量)移动构造,右值引用,要消亡的局部变量的所占用的资源转让给临时对象
IntNum(IntNum&& n) :xptr(n.xptr) {
n.xptr = nullptr;//现在xptr和n.ptr都指向同一个对象,可以让本要消亡的对象指向空,把资源让给需要的
cout << "Calling move constructor ..." << endl;
}
~IntNum() {
delete xptr;
cout << "Destructing ..." << endl;
}
int getInt() { return *xptr; }
private:
int* xptr;
};
IntNum getNum() {
//调用的是默认构造函数
IntNum a;
//局部变量a在函数运行结束后就会消亡,return时会调用复制构造函数,产生一个临时无名对象,这个临时无名对象时局部变量a的一个备份
return a;
}
int main() {
cout << getNum().getInt() << endl;
return 0;
}
结果
Calling constructor ...
Calling move constructor ...//只是用到了浅层复制构造 效率比较高
Destructing ...
1
Destructing ...
17 左值、右值、临时变量和无名对象
左值引用与右值引用
C++对于左值和右值没有标准定义,但是有一个被广泛认同的说法:
- 可以取地址的,有名字的,非临时的就是左值;
格式 : 类型 & 引用名 = 左值表达式; - 不能取地址的,没有名字的,临时的就是右值;
格式 : 类型 && 引用名 = 右值表达式; - 可见立即数(常量,例如 1 2,“s”),函数返回的值等都是右值;而非匿名
临时变量
什么是临时变量?
- C++真正的临时对象是不可见的匿名对象,不会出现在你的源码中,但是程序在运行时确实生成了这样的对象。
什么时候会产生临时变量?
(1)当函数返回对象的时候
(2)为了使函数调用成功而进行隐式类型转换的时候
什么是无名对象?
无名对象,也成临时对象(注意不是临时变量)指的是直接由构造函数产生,但是没有被任何符号所引用的对象。例如:string("abc"),这句话产生的就是一个无名对象,这个对象产生以后,没有什么办法使用它。但是对于string str("abc")来说,则产生的是一个有名字的对象,他的名字就是 str。
临时变量和临时对象区别
临时对象指代在不具名内存中产生的匿名对象,特点为右值,结束其表达式无价值。临时变量指代为某算法而引入的一些临时性的中间变量,特点为离开其作用域无价值。
//注意,由于tmp是局部变量,离开函数时会进行消亡,return时编译器会对象进行复制d构造,产生一个临时的无名对象。这里T不能是不完整类型。
template<typename T>
T function()
{
T tmp;
return tmp;
}
C++0x标准中引入新的右值引用类型,可以直接返回临时对象而不进行复制构造:
std::vector<int>&& func()
{
std::vector<int> retv;
// fill retv
return retv;
}