讨论C++类与对象

C语言结构体和C++类的对比

在C语言中,要想描述一个复杂结构,需要使用结构体。例如描述一个学生,需要有学号,学生姓名,年龄以及性别。

C语言中结构体

上述代码结果:

结构体输出结果

C语言中的结构体可以描述复杂结构,但想要修改就需要在外部定义函数,传入结构体指针,较为复杂。

void modifyStudent(struct Student* pst)
{
    printf("请输入想要修改的年龄:");
    int age = 0;
    scanf("%d", &age);
    pst->age = age;
}

修改结构体对象中的值

在C++中,引入了类的概念,它类似于C语言的结构体,可以描述一个复杂结构,不同于结构体,类中是可以定义成员函数的,可以极大方便对成员变量进行修改。

为了兼容C语言,C++中使用struct关键字可以定义类,也可以使用class关键字定义类。

struct关键字定义的类没有访问修饰限定符,默认都是public公开访问的。

class关键字定义的类可以自定访问修饰限定符,privateprotectedpublic,默认的访问权限是private私有的。类外不能访问。

class Student {
public:
    int _stuId;
    std::string _name;
    int _age;
    std::string _gender;
};

如上代码便定义出一个学生类,由于成员变量都是public公有的,所以在类外也可以访问。

类的实例化

用类类型创建对象的过程,称为类的实例化。

int main() {
    Student st1;
    st1._stuId = 101;
    st1._name = "张三";
    st1._age = 23;
    st1._gender = "男";

    std::cout << "学生> 学号:" << st1._stuId << ", 姓名:" << st1._name <<
    ", 年龄:" << st1._age << ", 性别:" << st1._gender << std::endl;
    return 0;
}

对象成员变量访问

  • 如果成员变量使用private修饰,类外就不能直接进行访问,会报错。

类对象的大小

类中既有成员变量,又有成员函数,那么类对象的存储方式是什么样的呢?

猜想一

成员变量和成员函数都存储在类对象中。

class Test1 {
public:
    void test() {}
private:
    int _a;
};

类对象存储猜想1

猜想二

成员变量存储在类对象中,成员函数存储在公共代码段中,由函数表记录。

类对象存储猜想2

针对上述猜想的实践

  • 使用sizeof计算类对象的大小。如果成员函数存储在类对象中,则对象所占内存大小一定大于int所占内存大小。
int main() {
    Test1 t1;
    std::cout << sizeof(t1) << std::endl;

    return 0;
}

类对象所占空间大小

由此可得出结论:只有成员变量存储在类对象中,成员函数存储在公共代码段中。

类对象所占空间大小也遵循内存对齐规则。

this指针

针对上述内容的讨论,可以得知每个实例化的对象都有一份自己的成员变量,但成员函数存放在公共代码段中,是所有对象共有的,但通过代码可知,通过不同对象调用成员函数,得到的成员变量是不同的。

不同对象调用成员函数

class Student {
public:
    void print() {
        std::cout << "学生> 学号:" << _stuId << ", 姓名:" << _name <<
    ", 年龄:" << _age << ", 性别:" << _gender << std::endl;
    }

public:
    int _stuId;
    std::string _name;
    int _age;
    std::string _gender;
};

int main() {
    Student st1;
    st1._stuId = 101;
    st1._name = "张三";
    st1._age = 23;
    st1._gender = "男";

    st1.print();

    Student st2;
    st2._stuId = 102;
    st2._name = "李四";
    st2._age = 24;
    st2._gender = "女";

    st2.print();

    return 0;
}

不同对象打印的值不同

  • 由此可见,在成员函数中一定有一个标识,表示不同的对象调用,这个标识就是***this指针*。

在每一个成员函数中,都有一个隐藏的参数,即this指针,该指针表示当前调用的对象。

this指针

类似于如上图所示,不过这个this指针不需要我们写出来,是编译器默认生成的,这就是即使成员函数不存放在对象中,不同的对象调用成员函数所显示的值会不一样了。

类的6个默认成员函数

默认成员函数即我们不写,编译器会自动生成的成员函数。

6个默认成员函数

构造函数

对于学生类,成员变量如果设为私有,类外便不能访问,实例化对象时,就不能直接赋值。若提供对外的getter & setter接口,也只能在实例化之后再一一赋值。如何达到实例化对象中就完成对对象成员变量的初始化,这就需要构造函数来完成。

构造函数是一个特殊的成员函数,函数名和类名相同,没有返回值,实例化对象时由编译器自动调用,且在对象的生命周期中只调用一次。

class Student {
public:
    Student() {
        std::cout << "无参的构造函数调用了" << std::endl;
    } // 无参的构造函数
    Student(int stuId, std::string name, int age, std::string gender) { // 有参的构造函数
        std::cout << "有参的构造函数调用了" << std::endl;
        _stuId = stuId;
        _name = name;
        _age = age;
        _gender = gender;
    }
    void print() {
        std::cout << "学生> 学号:" << _stuId << ", 姓名:" << _name <<
    ", 年龄:" << _age << ", 性别:" << _gender << std::endl;
    }

private:
    int _stuId;
    std::string _name;
    int _age;
    std::string _gender;
};


int main() {
    Student st1;
    st1.print();

    Student st2(102, "李四", 24, "女");
    st2.print();

    return 0;
}

构造函数

C++标准规定,若不对成员变量做初始化,编译器默认对内置类型不做处理,自定义类型会调用其默认的构造函数。

当然,如果我们不显示写构造函数,编译器会生成不带参数的构造函数,如果我们显示写了,编译器就不再生成不带参数的构造函数了。

class Student {
public:
//    Student() {
//        std::cout << "无参的构造函数调用了" << std::endl;
//    } // 无参的构造函数
    Student(int stuId, std::string name, int age, std::string gender) { // 有参的构造函数
        std::cout << "有参的构造函数调用了" << std::endl;
        _stuId = stuId;
        _name = name;
        _age = age;
        _gender = gender;
    }
    void print() {
        std::cout << "学生> 学号:" << _stuId << ", 姓名:" << _name <<
    ", 年龄:" << _age << ", 性别:" << _gender << std::endl;
    }

private:
    int _stuId;
    std::string _name;
    int _age;
    std::string _gender;
};


int main() {
    Student st1; // 此处会报错,因为显示写了带参数的构造函数,编译器不再生成不带参的构造函数。
    st1.print();

    Student st2(102, "李四", 24, "女");
    st2.print();

    return 0;
}

为了省事,不写那么多构造函数,我们也可以采用参数全缺省的形式来充当默认成员函数。

class Student {
public:
    Student(int stuId = 101, std::string name = "张三", int age = 23, std::string gender = "男") { // 有参的构造函数
        std::cout << "有参全缺省的构造函数调用了" << std::endl;
        _stuId = stuId;
        _name = name;
        _age = age;
        _gender = gender;
    }
    void print() {
        std::cout << "学生> 学号:" << _stuId << ", 姓名:" << _name <<
    ", 年龄:" << _age << ", 性别:" << _gender << std::endl;
    }

private:
    int _stuId;
    std::string _name;
    int _age;
    std::string _gender;
};

实例化对象时,若给初始值,则直接采用缺省值。

全缺省构造函数

析构函数

析构函数与构造函数的作用刚好相反,对象在销毁时自动调用析构函数,完成对象中资源的清理工作。

  • 语法:
    • 析构函数名与类名相同,在函数名前加~
    • 无返回值。
    • 一个类只能有一个析构函数,析构函数不能重载。
    • 对象生命周期结束时,编译器自动调用析构函数。
class Student {
public:
    Student(int stuId = 101, std::string name = "张三", int age = 23, std::string gender = "男") { // 有参的构造函数
        _stuId = stuId;
        _name = name;
        _age = age;
        _gender = gender;
    }
    void print() {
        std::cout << "学生> 学号:" << _stuId << ", 姓名:" << _name <<
    ", 年龄:" << _age << ", 性别:" << _gender << std::endl;
    }

    ~Student() {
        std::cout << "Student类的析构函数调用了" << std::endl;
    }

private:
    int _stuId;
    std::string _name;
    int _age;
    std::string _gender;
};


int main() {
    Student st1(102, "李四", 24, "女");
    st1.print();
    return 0;
}

析构函数

析构函数也是特殊的成员函数,因此对内置类型不做处理,自定义类型调用其析构函数。

拷贝构造函数

拷贝构造函数是构造函数的一个重载,参数只能有一个,且是类类型的引用。

拷贝构造函数

int main() {
    Student st1(102, "李四", 24, "女");
    st1.print();

    Student st2(st1); // 使用拷贝构造函数实例化对象
    st2.print();
    return 0;
}

拷贝构造函数打印结果

若未显示定义拷贝构造函数,编译器会自动生成默认的拷贝构造函数。对象会按照内存存储字节序完成拷贝,即浅拷贝。

浅拷贝和深拷贝

  • 如上述所说,浅拷贝只是对内存的直接复制,如果只是栈上开辟空间的变量,影响还没有那么大,但如果是在堆上开辟的空间,那么只拷贝值会影响非常大。
class Test {
public:
    Test() {
        _a = (int*)malloc(10 * sizeof(int));
        b = 20;
    }
    ~Test() {
        free(_a);
        _a = nullptr;
    }
private:
    int* _a;
    int b;
};

浅拷贝

默认生成的拷贝构造函数

  • 因此,面对这种情形,默认生成的拷贝构造函数就不能满足条件了,因此就需要自己显示定义拷贝构造函数,来达到深拷贝。

深拷贝

赋值运算符重载

C++为了增强代码的可读性,引入了运算符重载。

  • 语法:
    • 返回值类型 operator==(参数列表);
class Test {
public:
    Test() {
        _a = (int*)malloc(10 * sizeof(int));
        b = 20;
    }
    ~Test() {
        free(_a);
        _a = nullptr;
    }
    Test(const Test& t) {
        _a = (int*)malloc(10 * sizeof(int));
        for (int i = 0; i < 10; ++i) {
            _a[i] = t._a[i];
        }
    }
  	// 赋值运算符重载
    Test& operator=(const Test& t) {
        for (int i = 0; i < 10; ++i) {
            _a[i] = t._a[i]; // 深拷贝
        }
        return *this;
    }
private:
    int* _a;
    int b;
};

值得注意的是,我们常常看到这样的代码Test t2 = t1。这里虽然使用了=,但并不是赋值运算符重载,赋值运算符重载的定义是已实例化的对象被赋值,这上面代码是还没有实例化对象,所以是拷贝构造。

拷贝构造调试验证

初始化列表

实例化对象时,编译器会通过构造函数来给成员变量一个初始值,这种行为只能称为赋值,并不能称为初始化,因为初始化只有一次,而构造函数中可以多次赋值。

  • 语法:
    • :开始,用,分割数据成员列表,每个成员变量后面跟初始值(初始值)

初始化列表

初始化顺序

class Test {
public:
    Test()
            : _b(6), _a(_b) {}

    void print() {
        std::cout << _a << " " << _b << std::endl;
    }

private:
    int _a;
    int _b;
};


int main() {
    Test t;
    t.print();
    return 0;
}

如上述代码,按照初始化列表顺序初始化,则应是a = b = 6这个结果。

初始化列表初始化顺序

而真实的结果是_a是随机值,_b是预期值6

可得出结论: 成员变量的初始化顺序和初始化列表顺序无关,和声明顺序有关。

  • 23
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

烛九_阴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值