第六章 3指针

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。

image.png

指针与取指针变量的地址

假设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;

创建三维数组

image.png

创建动态多维数组总结

创建行不定 列确定和行不定 列也不确定的二维数组其实本质上没什么区别:申请空间时,必须要确定申请空间的最小单元。

  • 行不定,列确定。
//最小单元是一个固定长度一维数组
//每次申请一个确定的一维数组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;
}
image.png

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);

代码示例

image.png

image.png

image.png

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;
}

结果


image.png

错误分析


image.png

修改后

#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;
}
image.png

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;
}

理解的还是不太清楚,反正要记住的就是return 返回一个局部变量时,会自动拷贝一份局部变量,拷贝产生的这个变量就是临时变量,return返回这个临时变量

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。