C++核心知识(二)—— 类和对象(类的封装)、对象的构造和析构(浅拷贝、深拷贝、explicit、动态分配内存)

news2024/9/23 23:33:17

【上一篇】C++核心知识(一)—— C++概述、C++对C的扩展(作用域、struct类型、引用、内联函数、函数默认参数、函数占位参数、函数重载)

【下一篇】C++核心知识(三)—— 静态成员(变量、函数、const成员)、面向对象模型(this指针、常函数、常对象)、友元、数组类、单例模式

1. 类和对象的基本概念

1.1 C和C++中struct区别

  • c语言struct只有变量

  • c++语言struct 既有变量,也有函数

1.2 类的封装

我们编写程序的目的是为了解决现实中的问题,而这些问题的构成都是由各种事物组成,我们在计算机中要解决这种问题,首先要做就是要将这个问题的参与者:事和物抽象到计算机程序中,也就是用程序语言表示现实的事物。

那么现在问题是如何用程序语言来表示现实事物?现实世界的事物所具有的共性就是每个事物都具有自身的属性,一些自身具有的行为,所以如果我们能把事物的属性和行为表示出来,那么就可以抽象出来这个事物。

比如我们要表示人这个对象,在c语言中,我们可以这么表示:

typedef struct _Person{
    char name[64];
    int age;
}Person;
typedef struct _Aninal{
    char name[64];
    int age;
    int type; //动物种类
}Ainmal;

void PersonEat(Person* person){
    printf("%s在吃人吃的饭!\n",person->name);
}
void AnimalEat(Ainmal* animal){
    printf("%s在吃动物吃的饭!\n", animal->name);
}

int main(){

    Person person;
    strcpy(person.name, "小明");
    person.age = 30;
    AnimalEat(&person);

    return EXIT_SUCCESS;
}

定义一个结构体用来表示一个对象所包含的属性,函数用来表示一个对象所具有的行为,这样我们就表示出来一个事物,在c语言中,行为和属性是分开的,也就是说吃饭这个属性不属于某类对象,而属于所有的共同的数据,所以不单单是PeopleEat可以调用Person数据,AnimalEat也可以调用Person数据,那么万一调用错误,将会导致问题发生。

从这个案例我们应该可以体会到,属性和行为应该放在一起,一起表示一个具有属性和行为的对象。

假如某对象的某项属性不想被外界获知,比如说漂亮女孩的年龄不想被其他人知道,那么年龄这条属性应该作为女孩自己知道的属性;或者女孩的某些行为不想让外界知道,只需要自己知道就可以。那么这种情况下,封装应该再提供一种机制能够给属性和行为的访问权限控制住。

所以说封装特性包含两个方面,一个是属性和变量合成一个整体,一个是给属性和函数增加访问权限

  • 封装

  1. 把变量(属性)和函数(操作)合成一个整体,封装在一个类中

  1. 对变量和函数进行访问控制

  • 访问权限

  1. 在类的内部(作用域范围内),没有访问权限之分,所有成员可以相互访问

  1. 在类的外部(作用域范围外),访问权限才有意义:public,private,protected

  1. 在类的外部,只有public修饰的成员才能被访问,在没有涉及继承与派生时,private和protected是同等级的,外部不允许访问,子类内部可以访问父类的protected权限成员

//封装两层含义
//1. 属性和行为合成一个整体
//2. 访问控制,现实事物本身有些属性和行为是不对外开放
class Person{
//人具有的行为(函数)
public:
    void Dese(){ cout << "我有钱,年轻,个子又高,就爱嘚瑟!" << endl;}
//人的属性(变量)
public:
    int mTall; //多高,可以让外人知道
protected:
    int mMoney; // 有多少钱,只能儿子孙子知道
private:
    int mAge; //年龄,不想让外人知道
};

int main(){

    Person p;
    p.mTall = 220;
    //p.mMoney 保护成员外部无法访问
    //p.mAge 私有成员外部无法访问
    p.Dese();

    return EXIT_SUCCESS;
}
struct和class的区别?
class默认访问权限为private,struct默认访问权限为public.
class A{
    int mAge;
};
struct B{
    int mAge;
};

void test(){
    A a;
    B b;
    //a.mAge; //无法访问私有成员
    b.mAge; //可正常外部访问
}

1.3 将成员变量设置为private

  1. 可以控制属性的读写权限

使用成员函数可使得我们对变量的控制处理更加精细。如果我们让所有的成员变量为public,每个人都可以读写它。如果我们设置为private,我们可以实现“不准访问”、“只读访问”、“读写访问”,甚至你可以写出“只写访问”。

  1. 可赋予客户端访问数据的一致性。

如果成员变量不是public,客户端唯一能够访问对象的方法就是通过成员函数。如果类中所有public权限的成员都是函数,客户在访问类成员时只会默认访问函数,不需要考虑访问的成员需不需要添加(),这就省下了许多搔首弄耳的时间。

3.可以保护属性的合法性

2. 面向对象程序设计案例

2.1 设计立方体类

设计立方体类(Cube),求出立方体的面积( 2*a*b + 2*a*c + 2*b*c )和体积( a * b * c),分别用全局函数成员函数判断两个立方体是否相等。

//立方体类
class Cub{
public:
    void setL(int l){ mL = l; }
    void setW(int w){ mW = w; }
    void setH(int h){ mH = h; }
    int getL(){ return mL; }
    int getW(){ return mW; }
    int getH(){ return mH; }
    //立方体面积
    int caculateS(){ return (mL*mW + mL*mH + mW*mH) * 2; }
    //立方体体积
    int caculateV(){ return mL * mW * mH; }
    //成员方法
    bool CubCompare(Cub& c){
        if (getL() == c.getL() && getW() == c.getW() && getH() == c.getH()){
            return true;
        }
        return false;
    }
private:
    int mL; //长
    int mW; //宽
    int mH; //高
};

//比较两个立方体是否相等
bool CubCompare(Cub& c1, Cub& c2){
    if (c1.getL() == c2.getL() && c1.getW() == c2.getW() && c1.getH() == c2.getH()){
        return true;
    }
    return false;
}

void test(){
    Cub c1, c2;
    c1.setL(10);
    c1.setW(20);
    c1.setH(30);

    c2.setL(20);
    c2.setW(20);
    c2.setH(30);

    cout << "c1面积:" << c1.caculateS() << " 体积:" << c1.caculateV() << endl;
    cout << "c2面积:" << c2.caculateS() << " 体积:" << c2.caculateV() << endl;

    //比较两个立方体是否相等
    if (CubCompare(c1, c2)){
        cout << "c1和c2相等!" << endl;
    }
    else{
        cout << "c1和c2不相等!" << endl;
    }

    if (c1.CubCompare(c2)){
        cout << "c1和c2相等!" << endl;
    }
    else{
        cout << "c1和c2不相等!" << endl;
    }
}

2.2 点和圆的关系

设计一个圆形类(AdvCircle),和一个点类(Point),计算点和圆的关系。

假如圆心坐标为x0, y0, 半径为r,点的坐标为x1, y1:

  1. 点在圆上:(x1-x0)*(x1-x0) + (y1-y0)*(y1-y0) == r*r

  1. 点在圆内:(x1-x0)*(x1-x0) + (y1-y0)*(y1-y0) < r*r

  1. 点在圆外:(x1-x0)*(x1-x0) + (y1-y0)*(y1-y0) > r*r

//点类
class Point{
public:
    void setX(int x){ mX = x; }
    void setY(int y){ mY = y; }
    int getX(){ return mX; }
    int getY(){ return mY; }
private:
    int mX;
    int mY;
};

//圆类
class Circle{
public:
    void setP(int x,int y){
        mP.setX(x);
        mP.setY(y);
    }
    void setR(int r){ mR = r; }
    Point& getP(){ return mP; }
    int getR(){ return mR; }
    //判断点和圆的关系
    void IsPointInCircle(Point& point){
        int distance = (point.getX() - mP.getX()) * (point.getX() - mP.getX()) + (point.getY() - mP.getY()) * (point.getY() - mP.getY());
        int radius = mR * mR;
        if (distance < radius){
            cout << "Point(" << point.getX() << "," << point.getY() << ")在圆内!" << endl;
        }
        else if (distance > radius){
            cout << "Point(" << point.getX() << "," << point.getY() << ")在圆外!" << endl;
        }
        else{
            cout << "Point(" << point.getX() << "," << point.getY() << ")在圆上!" << endl;
        }
    }
private:
    Point mP; //圆心
    int mR; //半径
};

void test(){
    //实例化圆对象
    Circle circle;
    circle.setP(20, 20);
    circle.setR(5);
    //实例化点对象
    Point point;
    point.setX(25);
    point.setY(20);

    circle.IsPointInCircle(point);
}

3. 对象的构造和析构

3.1 初始化和清理

我们大家在购买一台电脑或者手机,或者其他的产品,这些产品都有一个初始设置,也就是这些产品对被创建的时候会有一个基础属性值。那么随着我们使用手机和电脑的时间越来越久,那么电脑和手机会慢慢被我们手动创建很多文件数据,某一天我们不用手机或电脑了,那么我们应该将电脑或手机中我们增加的数据删除掉,保护自己的信息数据。

从这样的过程中,我们体会一下,所有的事物在起初的时候都应该有个初始状态,当这个事物完成其使命时,应该及时清除外界作用于上面的一些信息数据。

那么我们c++中OO思想也是来源于现实,是对现实事物的抽象模拟,具体来说,当我们创建对象的时候,这个对象应该有一个初始状态,当对象销毁之前应该销毁自己创建的一些数据。

对象的初始化和清理也是两个非常重要的安全问题,一个对象或者变量没有初始时,对其使用后果是未知,同样的使用完一个变量,没有及时清理,也会造成一定的安全问题。c++为了给我们提供这种问题的解决方案,构造函数析构函数,这两个函数将会被编译器自动调用,完成对象初始化对象清理工作

无论你是否喜欢,对象的初始化和清理工作是编译器强制我们要做的事情,即使你不提供初始化操作和清理操作,编译器也会给你增加默认的操作,只是这个默认初始化操作不会做任何事,所以编写类就应该顺便提供初始化函数。

为什么初始化操作是自动调用而不是手动调用?既然是必须操作,那么自动调用会更好,如果靠程序员自觉,那么就会存在遗漏初始化的情况出现。

3.2 构造函数和析构函数

构造函数主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。

析构函数主要用于对象销毁前系统自动调用,执行一些清理工作。

构造函数语法:

  • 构造函数函数名和类名相同,没有返回值,不能有void,但可以有参数。

  • ClassName(){}

析构函数语法:

  • 析构函数函数名是在类名前面加”~”组成,没有返回值,不能有void,不能有参数,不能重载。

  • ~ClassName(){}

class Person{
public:
    Person(){
        cout << "构造函数调用!" << endl;
        pName = (char*)malloc(sizeof("John"));
        strcpy(pName, "John");
        mTall = 150;
        mMoney = 100;
    }
    ~Person(){
        cout << "析构函数调用!" << endl;
        if (pName != NULL){
            free(pName);
            pName = NULL;
        }
    }
public:
    char* pName;
    int mTall;
    int mMoney;
};

void test(){
    Person person;
    cout << person.pName << person.mTall << person.mMoney << endl;
}

3.3 构造函数的分类及调用

  • 按参数类型:分为无参构造函数和有参构造函数

  • 按类型分类:普通构造函数和拷贝构造函数(复制构造函数)

class Person{
public:
    Person(){
        cout << "no param constructor!" << endl;
        mAge = 0;
    }
    //有参构造函数
    Person(int age){
        cout << "1 param constructor!" << endl;
        mAge = age;
    }
    //拷贝构造函数(复制构造函数) 使用另一个对象初始化本对象
    Person(const Person& person){
        cout << "copy constructor!" << endl;
        mAge = person.mAge;
    }
    //打印年龄
    void PrintPerson(){
        cout << "Age:" << mAge << endl;
    }
private:
    int mAge;
};
//1. 无参构造调用方式
void test01(){
    
    //调用无参构造函数
    Person person1; 
    person1.PrintPerson();

    //无参构造函数错误调用方式
    //Person person2();
    //person2.PrintPerson();
}
//2. 调用有参构造函数
void test02(){
    
    //第一种 括号法,最常用
    Person person01(100);
    person01.PrintPerson();

    //调用拷贝构造函数
    Person person02(person01);
    person02.PrintPerson();

    //第二种 匿名对象(显示调用构造函数)
    Person(200); //匿名对象,没有名字的对象

    Person person03 = Person(300);
    person03.PrintPerson();

    //注意: 使用匿名对象初始化判断调用哪一个构造函数,要看匿名对象的参数类型
    Person person06(Person(400)); //等价于 Person person06 = Person(400);
    person06.PrintPerson();

    //第三种 =号法 隐式转换
    Person person04 = 100; //Person person04 =  Person(100)
    person04.PrintPerson();

    //调用拷贝构造
    Person person05 = person04; //Person person05 =  Person(person04)
    person05.PrintPerson();
}
b为A的实例化对象,A a = A(b) 和 A(b)的区别?
当A(b) 有变量来接的时候,那么编译器认为他是一个匿名对象,当没有变量来接的时候,编译器认为你A(b) 等价于 A b.

注意:不能调用拷贝构造函数去初始化匿名对象,也就是说以下代码不正确:

class Teacher{
public:
    Teacher(){
        cout << "默认构造函数!" << endl;
    }
    Teacher(const Teacher& teacher){
        cout << "拷贝构造函数!" << endl;
    }
public:
    int mAge;
};
void test(){
    
    Teacher t1;
    //error C2086:“Teacher t1”: 重定义
    Teacher(t1);  //此时等价于 Teacher t1;
}

3.4 拷贝构造函数的调用时机

  • 对象以值传递的方式传给函数参数

  • 函数局部对象以值传递的方式从函数返回 (vs debug模式下调用一次拷贝构造,qt不调用任何构造)

  • 用一个对象初始化另一个对象

class Person{
public:
    Person(){
        cout << "no param contructor!" << endl;
        mAge = 10;
    }
    Person(int age){
        cout << "param constructor!" << endl;
        mAge = age;
    }
    Person(const Person& person){
        cout << "copy constructor!" << endl;
        mAge = person.mAge;
    }
    ~Person(){
        cout << "destructor!" << endl;
    }
public:
    int mAge;
};
//1. 旧对象初始化新对象
void test01(){

    Person p(10);
    Person p1(p);
    Person p2 = Person(p);
    Person p3 = p; // 相当于Person p2 = Person(p);
}

//2. 传递的参数是普通对象,函数参数也是普通对象,传递将会调用拷贝构造
void doBussiness(Person p){}

void test02(){
    Person p(10);
    doBussiness(p);
}

//3. 函数返回局部对象
Person MyBusiness(){
    Person p(10);
    cout << "局部p:" << (int*)&p << endl;
    return p;
}
void test03(){
    //vs release、qt下没有调用拷贝构造函数
    //vs debug下调用一次拷贝构造函数
    Person p = MyBusiness();
    cout << "局部p:" << (int*)&p << endl;
}

Test03结果说明:

编译器存在一种对返回值的优化技术,RVO(Return Value Optimization).在vs debug模式下并没有进行这种优化,所以函数MyBusiness中创建p对象,调用了一次构造函数,当编译器发现你要返回这个局部的对象时,编译器通过调用拷贝构造生成一个临时Person对象返回,然后调用p的析构函数。

我们从常理来分析的话,这个匿名对象和这个局部的p对象是相同的两个对象,那么如果能直接返回p对象,就会省去一个拷贝构造和一个析构函数的开销,在程序中一个对象的拷贝也是非常耗时的,如果减少这种拷贝和析构的次数,那么从另一个角度来说,也是编译器对程序执行效率上进行了优化。

所以在这里,编译器偷偷帮我们做了一层优化:

当我们这样去调用: Personp = MyBusiness();

编译器偷偷将我们的代码更改为:

 void MyBussiness(Person& _result){
       _result.X:X(); //调用Person默认拷贝构造函数
       //.....对_result进行处理
       return;
   }
int main(){
   Person p; //这里只分配空间,不初始化
   MyBussiness(p);
}

3.5 构造函数调用规则

  • 默认情况下,c++编译器至少为我们写的类增加3个函数

  1. 默认构造函数(无参,函数体为空)

  1. 默认析构函数(无参,函数体为空)

  1. 默认拷贝构造函数,对类中非静态成员属性简单值拷贝

  • 如果用户定义拷贝构造函数,c++不会再提供任何默认构造函数

  • 如果用户定义了普通构造(非拷贝),c++不在提供默认无参构造,但是会提供默认拷贝构造

3.6 深拷贝和浅拷贝

3.6.1 浅拷贝

同一类型的对象之间可以赋值,使得两个对象的成员变量的值相同,两个对象仍然是独立的两个对象,这种情况被称为浅拷贝.

一般情况下,浅拷贝没有任何副作用,但是当类中有指针,并且指针指向动态分配的内存空间,析构函数做了动态内存释放的处理,会导致内存问题。

3.6.2 深拷贝

当类中有指针,并且此指针有动态分配空间,析构函数做了释放处理,往往需要自定义拷贝构造函数,自行给指针动态分配空间,深拷贝。

class Person{
public:
    Person(char* name,int age){
        pName = (char*)malloc(strlen(name) + 1);
        strcpy(pName,name);
        mAge = age;
    }
    //增加拷贝构造函数
    Person(const Person& person){
        pName = (char*)malloc(strlen(person.pName) + 1);
        strcpy(pName, person.pName);
        mAge = person.mAge;
    }
    ~Person(){
        if (pName != NULL){
            free(pName);
        }
    }
private:
    char* pName;
    int mAge;
};

void test(){
    Person p1("Edward",30);
    //用对象p1初始化对象p2,调用c++提供的默认拷贝构造函数
    Person p2 = p1;
}

3.7 多个对象构造和析构

3.7.1 初始化列表

构造函数和其他函数不同,除了有名字,参数列表,函数体之外还有初始化列表。

初始化列表简单使用:

class Person{
public:
#if 0
    //传统方式初始化
    Person(int a,int b,int c){
        mA = a;
        mB = b;
        mC = c;
    }
#endif
    //初始化列表方式初始化
    Person(int a, int b, int c):mA(a),mB(b),mC(c){}
    void PrintPerson(){
        cout << "mA:" << mA << endl;
        cout << "mB:" << mB << endl;
        cout << "mC:" << mC << endl;
    }
private:
    int mA;
    int mB;
    int mC;
};

注意:初始化成员列表(参数列表)只能在构造函数使用。

3.7.2 类对象作为成员

在类中定义的数据成员一般都是基本的数据类型。但是类中的成员也可以是对象,叫做对象成员

C++中对对象的初始化是非常重要的操作,当创建一个对象的时候,c++编译器必须确保调用了所有子对象的构造函数。如果所有的子对象有默认构造函数,编译器可以自动调用他们。但是如果子对象没有默认的构造函数,或者想指定调用某个构造函数怎么办?

那么是否可以在类的构造函数直接调用子类的属性完成初始化呢?但是如果子类的成员属性是私有的,我们是没有办法访问并完成初始化的。

解决办法非常简单:对于子类调用构造函数,c++为此提供了专门的语法,即构造函数初始化列表

当调用构造函数时,首先按各对象成员在类定义中的顺序(和参数列表的顺序无关)依次调用它们的构造函数,对这些对象初始化,最后再调用本身的函数体。也就是说,先调用对象成员的构造函数,再调用本身的构造函数

析构函数和构造函数调用顺序相反,先构造,后析构。

//汽车类
class Car{
public:
    Car(){
        cout << "Car 默认构造函数!" << endl;
        mName = "大众汽车";
    }
    Car(string name){
        cout << "Car 带参数构造函数!" << endl;
        mName = name;
    }
    ~Car(){
        cout << "Car 析构函数!" << endl;
    }
public:
    string mName;
};

//拖拉机
class Tractor{
public:
    Tractor(){
        cout << "Tractor 默认构造函数!" << endl;
        mName = "爬土坡专用拖拉机";
    }
    Tractor(string name){
        cout << "Tractor 带参数构造函数!" << endl;
        mName = name;
    }
    ~Tractor(){
        cout << "Tractor 析构函数!" << endl;
    }
public:
    string mName;
};

//人类
class Person{
public:
#if 1
    //类mCar不存在合适的构造函数
    Person(string name){
        mName = name;
    }
#else
    //初始化列表可以指定调用构造函数
    Person(string carName, string tracName, string name) : mTractor(tracName), mCar(carName), mName(name){
        cout << "Person 构造函数!" << endl;
    }
#endif
    
    void GoWorkByCar(){
        cout << mName << "开着" << mCar.mName << "去上班!" << endl;
    }
    void GoWorkByTractor(){
        cout << mName << "开着" << mTractor.mName << "去上班!" << endl;
    }
    ~Person(){
        cout << "Person 析构函数!" << endl;
    }
private:
    string mName;
    Car mCar; //编译只能调用无参的构造
    Tractor mTractor;
};

void test(){
    //Person person("宝马", "东风拖拉机", "赵四");
    Person person("刘能");
    person.GoWorkByCar();
    person.GoWorkByTractor();
}

3.8 explicit关键字

C++提供了关键字explicit,禁止通过构造函数进行的隐式转换。声明为explicit的构造函数不能在隐式转换中使用。

explicit注意:

  • explicit用于修饰构造函数,防止隐式转化。

  • 是针对单参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参构造)而言。

class MyString{
public:
    explicit MyString(int n){
        cout << "MyString(int n)!" << endl;
    }
    MyString(const char* str){
        cout << "MyString(const char* str)" << endl;
    }
};

int main(){

    //给字符串赋值?还是初始化?
    //MyString str1 = 1; 
    MyString str2(10);

    //寓意非常明确,给字符串赋值
    MyString str3 = "abcd";
    MyString str4("abcd");

    return EXIT_SUCCESS;
}

3.9 动态对象创建

当我们创建数组的时候,总是需要提前预定数组的长度,然后编译器分配预定长度的数组空间,在使用数组的时,会有这样的问题,数组也许空间太大了,浪费空间,也许空间不足,所以对于数组来讲,如果能根据需要来分配空间大小再好不过。

所以动态的意思意味着不确定性。

为了解决这个普遍的编程问题,在运行中可以创建和销毁对象是最基本的要求。当然c早就提供了动态内存分配(dynamic memory allocation),函数malloc和free可以在运行时从堆中分配存储单元。

然而这些函数在c++中不能很好的运行,因为它不能帮我们完成对象的初始化工作。

3.9.1 对象创建

当创建一个c++对象时会发生两件事:

  1. 为对象分配内存

  1. 调用构造函数来初始化那块内存

第一步我们能保证实现,需要我们确保第二步一定能发生。c++强迫我们这么做是因为使用未初始化的对象是程序出错的一个重要原因。

3.9.2 C动态分配内存方法

为了在运行时动态分配内存,c在他的标准库中提供了一些函数,malloc以及它的变种calloc和realloc,释放内存的free,这些函数是有效的、但是原始的,需要程序员理解和小心使用。为了使用c的动态内存分配函数在堆上创建一个类的实例,我们必须这样做:

class Person{
public:
    Person(){
        mAge = 20;
        pName = (char*)malloc(strlen("john")+1);
        strcpy(pName, "john");
    }
    void Init(){
        mAge = 20;
        pName = (char*)malloc(strlen("john")+1);
        strcpy(pName, "john");
    }
    void Clean(){
        if (pName != NULL){
            free(pName);
        }
    }
public:
    int mAge;
    char* pName;
};
int main(){

    //分配内存
    Person* person = (Person*)malloc(sizeof(Person));
    if(person == NULL){
        return 0;
    }
    //调用初始化函数
    person->Init();
    //清理对象
    person->Clean();
    //释放person对象
    free(person);

    return EXIT_SUCCESS;
}

问题:

  1. 程序员必须确定对象的长度。

  1. malloc返回一个void*指针,c++不允许将void*赋值给其他任何指针,必须强转。

  1. malloc可能申请内存失败,所以必须判断返回值来确保内存分配成功。

  1. 用户在使用对象之前必须记住对他初始化,构造函数不能显示调用初始化(构造函数是由编译器调用),用户有可能忘记调用初始化函数。

C的动态内存分配函数太复杂,容易令人混淆,是不可接受的,c++中我们推荐使用运算符new 和delete.

3.9.3 new operator

C++中解决动态内存分配的方案是把创建一个对象所需要的操作都结合在一个称为new的运算符里。当用new创建一个对象时,它就在堆里为对象分配内存并调用构造函数完成初始化

Person* person = new Person;
// 相当于:
Person* person = (Person*)malloc(sizeof(Person));
    if(person == NULL){
        return 0;
    }
person->Init();

New操作符能确定在调用构造函数初始化之前内存分配是成功的,所有不用显式确定调用是否成功。

现在我们发现在堆里创建对象的过程变得简单了,只需要一个简单的表达式,它带有内置的长度计算、类型转换和安全检查。这样在堆创建一个对象和在栈里创建对象一样简单。

3.9.4 delete operator

new表达式的反面是delete表达式。delete表达式先调用析构函数,然后释放内存。正如new表达式返回一个指向对象的指针一样,delete需要一个对象的地址。

delete只适用于由new创建的对象。

如果使用一个由malloc或者calloc或者realloc创建的对象使用delete,这个行为是未定义的。因为大多数new和delete的实现机制都使用了malloc和free,所以很可能没有调用析构函数就释放了内存。

如果正在删除的对象的指针是NULL,将不发生任何事,因此建议在删除指针后,立即把指针赋值为NULL,以免对它删除两次,对一些对象删除两次可能会产生某些问题。

class Person{
public:
    Person(){
        cout << "无参构造函数!" << endl;
        pName = (char*)malloc(strlen("undefined") + 1);
        strcpy(pName, "undefined");
        mAge = 0;
    }
    Person(char* name, int age){
        cout << "有参构造函数!" << endl;
        pName = (char*)malloc(strlen(name) + 1);
        strcpy(pName, name);
        mAge = age;
    }
    void ShowPerson(){
        cout << "Name:" << pName << " Age:" << mAge << endl;
    }
    ~Person(){
        cout << "析构函数!" << endl;
        if (pName != NULL){
            delete pName;
            pName = NULL;
        }
    }
public:
    char* pName;
    int mAge;
};

void test(){
    Person* person1 = new Person;
    Person* person2 = new Person("John",33);

    person1->ShowPerson();
    person2->ShowPerson();

    delete person1;
    delete person2;
}

3.9.5 用于数组的new和delete

使用new和delete在堆上创建数组非常容易。

//创建字符数组
char* pStr = new char[100];
//创建整型数组
int* pArr1 = new int[100]; 
//创建整型数组并初始化
int* pArr2 = new int[10]{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

//释放数组内存
delete[] pStr;
delete[] pArr1;
delete[] pArr2;

当创建一个对象数组的时候,必须对数组中的每一个对象调用构造函数,除了在栈上可以聚合初始化,必须提供一个默认的构造函数。

class Person{
public:
    Person(){
        pName = (char*)malloc(strlen("undefined") + 1);
        strcpy(pName, "undefined");
        mAge = 0;
    }
    Person(char* name, int age){
        pName = (char*)malloc(sizeof(name));
        strcpy(pName, name);
        mAge = age;
    }
    ~Person(){
        if (pName != NULL){
            delete pName;
        }
    }
public:
    char* pName;
    int mAge;
};

void test(){
    //栈聚合初始化
    Person person[] = { Person("john", 20), Person("Smith", 22) };
    //大部分编译器不支持这种写法,(聚合初始化)
    //Person *ms2 = new Person[2]{Person("john", 20), Person("Smith", 22)};
    cout << person[1].pName << endl;
    //创建堆上对象数组必须提供构造函数
    Person* workers = new Person[20]; //自动调用20次无参构造函数
}

3.9.6 delete void*可能会出错

如果对一个void*指针执行delete操作,这将可能成为一个程序错误,除非指针指向的内容是非常简单的,因为它将不执行析构函数.以下代码未调用析构函数,导致可用内存减少。

class Person{
public:
    Person(char* name, int age){
        pName = (char*)malloc(sizeof(name));
        strcpy(pName,name);
        mAge = age;
    }
    ~Person(){
        if (pName != NULL){
            delete pName;
        }
    }
public:
    char* pName;
    int mAge;
};

void test(){
    void* person = new Person("john",20);
    delete person; //err
}

问题:

malloc、free和new、delete可以混搭使用吗?也就是说malloc分配的内存,可以调用delete吗?通过new创建的对象,可以调用free来释放吗?

C和C++的申请和释放堆区空间不要混用

3.9.7 使用new和delete采用相同形式

    Person* person = new Person[10];
    delete person;

以上代码有什么问题吗?(vs下直接中断、qt下析构函数调用一次)

使用了new也搭配使用了delete,问题在于Person有10个对象,那么其他9个对象可能没有调用析构函数,也就是说其他9个对象可能删除不完全,因为它们的析构函数没有被调用。

我们现在清楚使用new的时候发生了两件事: 一、分配内存;二、调用构造函数,那么调用delete的时候也有两件事:一、析构函数;二、释放内存。

那么刚才我们那段代码最大的问题在于:person指针指向的内存中到底有多少个对象,因为这个决定应该有多少个析构函数应该被调用。换句话说,person指针指向的是一个单一的对象还是一个数组对象,由于单一对象和数组对象的内存布局是不同的。更明确的说,数组所用的内存通常还包括“数组大小记录”,使得delete的时候知道应该调用几次析构函数。单一对象的话就没有这个记录。单一对象和数组对象的内存布局可理解为下图:

本图只是为了说明,编译器不一定如此实现,但是很多编译器是这样做的。

当我们使用一个delete的时候,我们必须让delete知道指针指向的内存空间中是否存在一个“数组大小记录”的办法就是我们告诉它。当我们使用delete[],那么delete就知道是一个对象数组,从而清楚应该调用几次析构函数。

结论:

如果在new表达式中使用[],必须在相应的delete表达式中也使用[].如果在new表达式中不使用[], 一定不要在相应的delete[]表达式中使用[].

3.10 C++ 和C的共同和区别之处

  • 共同:都是申请堆区空间和释放堆区空间

  • 区别:C++的会调用构造和析构函数

【上一篇】C++核心知识(一)—— C++概述、C++对C的扩展(作用域、struct类型、引用、内联函数、函数默认参数、函数占位参数、函数重载)

【下一篇】C++核心知识(三)—— 静态成员(变量、函数、const成员)、面向对象模型(this指针、常函数、常对象)、友元、数组类、单例模式

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/392334.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

linux安装pycharm

linux安装pycharm1.下载相关软件包2. 安装步骤2.1 解压文件2.2 开启命令2.4 创建快捷方式官网链接 https://www.jetbrains.com/pycharm/download/#sectionlinux 1.下载相关软件包 找到自己下载的版本下载 2. 安装步骤 2.1 解压文件 进入压缩包路径 解压文件【我指定了解…

设计模式-第10章(建造者模式)

建造者模式建造者模式建造者模式解析建造者模式的基本代码结构建造者模式 建造者模式&#xff08;Builder&#xff09;&#xff0c;将一个复杂对象的构建和表示分离&#xff0c;使得同样的构建过程可以创建不同的表示。 如果我们用了建造者模式&#xff0c;那么用户就只需指定…

欢乐消除开心假日协议解密

欢乐消除开心假日协议解密协/议/流/量/解/密分析欢乐消除开心假日这款游戏流量的协议加密方式。序欢乐消除开心假日是一款合成模拟家装的游戏&#xff0c;在这个游戏中&#xff0c;你将成为一位充满热情的设计师&#xff0c;与好友一起经营工作室。你需要根据客户的需求重新设计…

详解PyTorch Tensor 数据类型——张量

目录 张量 张量的结构 张量的创建 张量的操作 数学运算 逻辑运算 形状操作 索引和切片 张量的维度变换 常见的数据类型存储 附录PyTorch官方API中文文档 张量 在PyTorch中&#xff0c;张量&#xff08;Tensor&#xff09;是一种常见的数据类型&#xff0c;它是一个…

如何快速将多图片合并拼接成一张大图并保留原图质量 - 极速图片合成拼接器软件简介

在工作中我们有时需要把多张图片打印在一张纸上&#xff0c;那就必须手动排列和调整每张图片的大小&#xff0c;这可是一项费时费力的任务。 借助极速图片合成拼接器这款软件&#xff0c;终于可以快速自动解决这种任务啦&#xff01; 它是一款可以自动一次性将多个图像文件合并…

Jenkins部署angular11自动打包

可能年纪大了&#xff0c;对于新东西的学习和接收有点慢&#xff0c;花了差不多一周的时间&#xff0c;终于把jenkins配置好了&#xff0c;可以自动打包&#xff0c;与手动打出来的一样&#xff0c;以后就解放双手了。#!/bin/bashnpm cache clean -fnpm -vnode -vnpm install n…

ubuntu 18.04.06LST安装R4.0+版本报错及解决过程

1. sudo apt-get update无法正常使用 错误:13 http://ppa.launchpad.net/webupd8team/sublime-text-3/ubuntu bionic Release 404 Not Found [IP: 2620:2d:4000:1::3e 80] 解决措施&#xff1a;删除 webupd8team/sublime-text-3这个ppa文件。 sudo add-apt-repository --…

MySQL调优 - SQL查询深度分页问题

一、问题引入 例如当前存在一张表test_user&#xff0c;然后往这个表里面插入3百万的数据&#xff1a; CREATE TABLE test_user (id int(11) NOT NULL AUTO_INCREMENT COMMENT 主键id,user_id varchar(36) NOT NULL COMMENT 用户id,user_name varchar(30) NOT NULL COMMENT 用…

嵌入式学习笔记——寄存器开发STM32 GPIO口

寄存器开发STM32GPIO口前言认识GPIOGPIO是什么GPIO有什么用GPIO怎么用STM32上GPIO的命名以及数量GPIO口的框图&#xff08;重点&#xff09;输入框图解析三种输入模式GPIO输入时内部器件及其作用1.保护二极管2.上下拉电阻&#xff08;可配置&#xff09;3.施密特触发器4.输入数…

什么是CatGPT-使用效果如何-

个人使用效果&#xff0c;评分优&#xff0c;足以满足教学和填表。程序媛借助CatGPT&#xff08;ChatGPT更佳&#xff09;&#xff0c;基本上可以秒杀不用此类工具的程序猿&#xff08;男&#xff09;&#xff01;&#xff01;&#xff01;问&#xff1a;为什么使用AIGC能大幅度…

79、ClimateNeRF: Physically-based Neural Rendering for Extreme Climate Synthesis

简介主页物理模拟可以很好地预测天气影响。神经辐射场产生SOTA场景模型。ClimateNeRF 允许我们渲染真实的天气效果&#xff0c;包括雾霾、雪和洪水 &#xff0c;结果可以通过有物理意义的变量来控制&#xff0c;比如水位 &#xff0c;这允许人们可视化气候变化的结果将对他们产…

2022年MathorCup数学建模B题无人仓的搬运机器人调度问题解题全过程文档加程序

2022年第十二届MathorCup高校数学建模 B题 无人仓的搬运机器人调度问题 原题再现 本题考虑在无人仓内的仓库管理问题之一&#xff0c;搬运机器人 AGV 的调度问题。更多的背景介绍请参看附件-背景介绍。对于无人仓来说&#xff0c;仓库的地图模型可以简化为图的数据结构。 仓库…

面试常问【进程间通信】最详细解说

文章目录 目录 文章目录 一、进程间有哪几种的通信方式 1.为什么需要进程间通信? 2.进程通信的几种方式 3.进程间通信的目的 二、管道 1.匿名管道 2.命名管道 3.管道总结 三、消息队列 四、共享内存 1.共享内存的原理 2.共享内存的接口 1.创建共享内存 2.将共享内存附加到进程…

HBase---Hbase安装(单机版)

Hbase安装单机版 文章目录Hbase安装单机版Master/Slave架构安装步骤配置Hbase1.上传压缩包解压更名修改hbase-env.sh修改hbase-site.xml配置HBase环境变量配置Zookeeper复制配置文件修改zoo.cfg配置文件修改myid配置Zookeeper环境变量刷信息配置文件启动hbase步骤hbase shellMa…

BS系统中的安全方案(SSO和Oauth2认证,数据加密)

摘要用户用浏览器打开网站&#xff0c;DNS会根据域名找到相应的服务器IP给到浏览器&#xff0c;仅接着用户的浏览器会与服务器建立连接&#xff0c;通过网路上的各个设备(交换机、路由器、基站、光纤等)&#xff0c;将服务器上的数据发送到用户的电脑上&#xff0c;在浏览器里呈…

运输层概述及web请求

运输层 运输层概述 运输层向高层用户屏蔽了下面网络核心的细节&#xff08;如网络拓扑、所采用的路由选择协议等&#xff09;它使应用进程看见的就好像是在两个运输层实体之间有一条端到端的逻辑通信信道&#xff1b; 根据需求不同&#xff0c;运输层提供两种运输协议 面向连…

buffer它到底做了个啥,源码级分析linux内核的文件系统的缓冲区

最近一直在学习linux内核源码&#xff0c;总结一下 https://github.com/xiaozhang8tuo/linux-kernel-0.11 自己整理过的带注释的源码。 为什么要有buffer ​ 高速缓冲区是文件系统访问块设备中数据的必经要道(PS:如果所有程序结果都不落盘&#xff0c;只是int a, a直接在主存…

TryHackMe-Debug(ez php反序列化)

Debug Linux机器CTF&#xff01;您将了解枚举&#xff0c;查找隐藏的密码文件以及如何利用php反序列化&#xff01; 端口扫描 循例&#xff0c;nmap Web枚举 进到web是apache默认页面&#xff0c;直接开扫 由于题目告诉我们涉及php反序列化&#xff0c;那直接找php文件来看&…

Linux学习第十四节-shell脚本

1.Shell概述 Shell连接了用户和Linux内核&#xff0c;他可以解释用户输入的命令传输给内核&#xff0c;让用户可以更加方便的使用Linux系统&#xff1b; Shell本身并不是内核的一部分&#xff0c;他只是站在内核的基础上编写一个应用程序&#xff1b; Shell具备编程的能力&a…

C++回顾(八)—— 继承

8.1 继承的概念 继承是类与类之间的关系&#xff0c;是一个很简单很直观的概念&#xff0c;与现实世界中的继承类似&#xff0c;例如儿子继承父亲的财产。 继承&#xff08;Inheritance&#xff09;可以理解为一个类从另一个类获取成员变量和成员函数的过程。例如类 B 继承于类…