c++基础

本文介绍了C++中的类和对象概念,包括属性和行为。讲解了如何创建栈内存和堆内存对象,以及对象实例化的两种方式。接着探讨了封装,通过getter和setter实现属性访问控制。接着深入构造函数,包括基本使用、构造初始化列表和拷贝构造函数,强调了深拷贝与浅拷贝的区别。最后,讨论了析构函数在释放资源和避免内存泄漏中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、类与对象

1.1 概念

类:类是抽象的概念,规定了同一类对象拥有的属性行为(成员=属性+行为)

对象:对象是具体的,是按照类的概念创建出来的实体

1.2 类的定义

【例子】以“手机”为例,来说明类的定义。

手机的属性:

品牌、型号、重量

手机的行为:

播放音乐、运行游戏、通讯

1.3 对象实例化

C++有两种创建对象的方式:

1.3.1 栈内存对象

这种对象创建后,在其生命周期结束后(所在的花括号执行完成后),自动被销毁。

#include <iostream>

using namespace std;

class MobilePhone
{
public:
    string brand;
    string model;
    int weight;

    void play_music()
    {
        cout << "我在遥望,月亮之上~" << endl;
    }

    void run_game()
    {
        cout << "Timi" << endl;
    }

    void communicate()
    {
        cout << "喂?" << endl;
    }
};

int main()
{
    // 创建栈内存对象
    MobilePhone mp1;
    // 调用属性
    mp1.brand = "小米";
    mp1.model = "12";
    mp1.weight = 199;
    cout << mp1.brand << " " << mp1.model << " "
            << mp1.weight << endl;
    // 调用成员函数
    mp1.play_music();
    mp1.run_game();
    mp1.communicate();

    return 0;
} // 主函数执行完成后 mp1自动销毁

1.3.2 堆内存对象

堆内存对象的创建需要使用new关键字,并且使用指针来指向此对象。

堆内存的对象需要程序员手动使用delete关键字来销毁,如果没有delete,那么这个对象所在的花括号执行完成后,会持续占用内存且无法回收,这种现象被称为“内存泄漏”。轻微的内存泄漏并不会对程序运行有明显的影响,随着内存泄漏的累积,逐渐会导致程序卡顿,甚至无法正常运行。

因此对于堆内存对象,通常有new就一定有一个对应的delete。

已经delete的对象,有时候还可以正常使用其部分功能,但是请不要这要做!

栈内存对象不支持delete关键字。

#include <iostream>

using namespace std;

class MobilePhone
{
public:
    string brand;
    string model;
    int weight;

    void play_music()
    {
        cout << "我在遥望,月亮之上~" << endl;
    }

    void run_game()
    {
        cout << "Timi" << endl;
    }

    void communicate()
    {
        cout << "喂?" << endl;
    }
};

int main()
{
    // 创建堆内存对象
    MobilePhone* mp2 = new MobilePhone;
    // 调用属性
    mp2->brand = "华为";
    mp2->model = "P50";
    mp2->weight = 201;
    cout << mp2->brand << " " << mp2->model << " "
            << mp2->weight << endl;
    // 调用成员函数
    mp2->play_music();
    mp2->run_game();
    mp2->communicate();

    // 手动销毁mp2
    delete mp2;
    return 0;
}

、封装

封装指的是,先将类的一些属性和其它细节隐藏,根据业务需求重新提供给外部对应功能的调用接口。

外部的调用接口最基础的有两类:

  • 读取属性值 getter

  • 写入属性值 setter

封装前后的类,类似于测试中的白盒与黑盒的概念。

以之前的手机类为例,来进行封装的演示。

#include <iostream>

using namespace std;

class MobilePhone
{
private: // 私有权限:只能在类内部访问
    string brand; // 只读:只添加getter接口
    string model; // 只写:只添加setter接口
    int weight; //  可读可写:同时添加getter和setter接口
public: // 重新对外部开放对应的功能接口
    string get_brand()
    {
        return brand;
    }

    void set_model(string m)
    {
        model = m;
    }

    int get_weight()
    {
        return weight;
    }

    void set_weight(int w)
    {
        weight = w;
    }
};

int main()
{
    // 创建栈内存对象
    MobilePhone mp1;
    // 通过公开接口访问属性
    mp1.set_weight(177);
    mp1.set_model("山寨");
    // 存在一个问题:brand属性默认值是空的
    cout << mp1.get_brand() << " " << mp1.get_weight() << endl;

    return 0;
}

三、构造函数

3.1 构造函数的基本使用

构造函数用来创建一个类的对象,在创建对象时还可以给对象的属性赋予初始值。

构造函数不用写返回值类型,函数名称必须与类名完全一致。

当程序员没有手动编写构造函数时,编译器会自动添加一个没有参数的,函数体为空的构造函数。一旦程序员手动编写了任一一个构造函数,编译器不再自动添加构造函数。

构造函数也支持函数重载和函数参数默认值。

#include <iostream>

using namespace std;

class MobilePhone
{
private:
    string brand;
    string model;
    int weight;
public:
    // 编译器自动添加的构造函数
//    MobilePhone(){}
    // 手动添加一个构造函数,来给属性赋予初始值
    MobilePhone(string b,string m,int w)
    {
        brand = b;
        model = m;
        weight = w;
    }

    // 函数重载
    MobilePhone()
    {
        brand = "山寨";
        model = "8848钛金手机";
        weight = 666;
    }

    // 打印当前的属性值
    void show()
    {
        cout << brand << " " << model << " " << weight << endl;
    }
};

int main()
{
    MobilePhone mp1("苹果","14 Pro",188);
    mp1.show();

    MobilePhone* mp2 = new MobilePhone("小米","12",199);
    mp2->show();
    delete mp2;

    MobilePhone mp3;
    mp3.show();

    return 0;
}

3.2 构造初始化列表

是基于构造函数对属性赋予初始值的另一种简便写法,在现有阶段,可以根据实际情况来决定是否采用。

#include <iostream>

using namespace std;

class MobilePhone
{
private:
    string brand;
    string model;
    int weight;
public:

    // 构造初始化列表
    MobilePhone(string b,string m,int w)
        :brand(b),model(m),weight(w){}

    void show()
    {
        cout << brand << " " << model << " " << weight << endl;
    }
};

int main()
{
    MobilePhone mp1("苹果","14 Pro",188);
    mp1.show();

    return 0;
}

关于构造初始化列表,需要注意以下几点:

  • 构造初始化列表的效率比构造函数函数体中赋值更高。

  • 如果成员变量是常成员变量,则不能在构造函数的函数体中赋予初始值,可以通过构造初始化列表赋予初始值。

  • 如果影响代码的可读写,可以不使用构造初始化列表。

3.3 拷贝构造函数

如果程序员不手动编写拷贝构造函数,编译器会为每个类增加一个默认的拷贝构造函数。

#include <iostream>

using namespace std;

class MobilePhone
{
private:
    // 设置属性的默认值
    string brand  = "山寨";
    string model = "8848";
    int weight = 188;
public:
    void show()
    {
        cout << brand << " " << model << " " << weight << endl;
    }
};

int main()
{
    MobilePhone mp1;
    mp1.show();

    // 调用默认的拷贝构造函数
    MobilePhone mp2(mp1);
    mp2.show();

    cout << &mp1 << " " << &mp2 << endl; // 0x61fe84 0x61fe78

    return 0;
}

上面的例子中,mp1和mp2是两个数据相同的对象,每个对象的数据都是相互独立,只属于当前对象持有。

3.3.1 浅拷贝

当成员变量出现指针类型时,默认的拷贝构造函数是基于赋值操作的,因此在拷贝的过程中,会出现指针的拷贝,会造成多个对象的属性指向同一个内存区域的问题,这样的现象不符合面向对象的设计规范。

#include <iostream>
#include <string.h>

using namespace std;

class Dog
{
private:
    char* name; // 故意使用char*

public:
    Dog(char* n)
    {
        name = n;
    }

    // 编译器自动添加下面的拷贝构造函数
    Dog(const Dog& d)
    {
        name = d.name; // 指针的拷贝!
    }

    void show_name()
    {
        cout << name << endl;
    }
};

int main()
{
    char c[20] = "WangCai";

    Dog d1(c);
    Dog d2(d1); // 拷贝构造函数

    strcpy(c,"XiaoBai");

    // 属性值无法对象独自持有
    d1.show_name(); // XiaoBai
    d2.show_name(); // XiaoBai

    return 0;
}

3.3.2 深拷贝

默认的拷贝是浅拷贝,当属性出现指针类型,应该手写一个深拷贝构造函数。

#include <iostream>
#include <string.h>

using namespace std;

class Dog
{
private:
    char* name;

public:
    Dog(char* n)
    {
        // 创建堆内存对象
        name = new char[20];
        // 数据复制
        strcpy(name,n);
    }

    Dog(const Dog& d)
    {
        // 创建堆内存对象
        name = new char[20];
        // 数据复制
        strcpy(name,d.name);
    }

    void show_name()
    {
        cout << name << endl;
    }
};

int main()
{
    char c[20] = "WangCai";

    Dog d1(c);
    Dog d2(d1); // 拷贝构造函数

    strcpy(c,"XiaoBai");

    // 属性值无法对象独自持有
    d1.show_name(); // WangCai
    d2.show_name(); // WangCai

    return 0;
}

在实际开发过程中,解决浅拷贝的问题可以通过一些简单粗暴的方式,例如直接把拷贝构造函数设置为私有权限,这样就屏蔽了外界对拷贝构造函数的调用。

ps.开辟的内存要记得释放

3.4 隐式调用构造函数

之前的例子都是显式调用构造函数,下面的例子来演示隐式调用构造函数。

#include <iostream>

using namespace std;

class Test
{
private:
    string name;

public:
    Test(string n):name(n)
    {
        cout << "调用了构造函数!" << endl;
    }

    string get_name()
    {
        return name;
    }
};

int main()
{
    string s1 = "AAA";
    string s2 = "BBB";
    // 显式调用构造函数
    Test t1(s1);
    cout << t1.get_name() << endl;
    // 隐式调用构造函数
    Test t2 = s2;
    cout << t2.get_name() << endl;

    return 0;
}

尽管隐式调用构造函数比较方便,但是可能会存在一个问题:在参数传递的过程中无意中创建了对象,这种情况可以使用explicit关键字修饰构造函数,来达到屏蔽使用构造函数的隐式调用。

四、析构函数

在3.3.2节中,深拷贝的示例代码虽然解决了指针作为成员变量拷贝对象的问题,但是在构造函数中开辟的内存区域,没有得到合理的释放,造成了内存泄漏的问题。要解决这个问题需要使用析构函数。

先来学习析构函数的基本使用,析构函数是与构造函数完全对立的函数。

构造函数

析构函数

调用时机:对象创建时

调用时机:销毁对象时

手动调用

自动调用

可以有参数,可以重载

不能有参数,不能重载

函数名是类名

函数名是~类名

通常用于对象数据的初始化

通常用于对象占用资源的释放

#include <iostream>

using namespace std;

class Test
{
private:
    string name;

public:
    Test(string n):name(n){}

    ~Test()
    {
        cout << name << "析构函数" << endl;
    }
};

int main()
{
    cout << "--------start--------" << endl;

    // 创建一个栈内存对象
    Test t1("A");
    // 创建一个堆内存对象
    Test* t2 = new Test("B");
    delete t2;

    cout << "--------finish--------" << endl;
    return 0;
}

运行结果如下:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值