自学内容网 自学内容网

C++——类和对象

1:类的定义

1.1:类的创建

class date
{
public:
void Days(int year, int month, int day)
{
_year = year;
_month = _month;
_day = day;
}

void print()
{
std::cout << _year << "/" << _month << "/" << _day << std::endl;
}

private:

int _year;
int _month;
int _day;
};

注:

①:class的创建与C语言中的struct创建类似

②:在C++中,对struct做了升级,除了能像class一样封装函数外,只需要结构体名称就可以代表类型,如下:

struct ListNodeCPP
{
    void Init(int x)
    {
        next = nullptr;
        val = x;
    }
    ListNodeCPP* next;//C语言中必须以 struct ListNodeCPP * next 这种方式定义
    int val;
};

1.2:访问限定符

class中存在访问限定:private、public、protected;默认不加访问限定符时,class 默认所有类中的变量都是 private,外界无法访问;而struct 默认所有结构体中的变量都是 public,外界可以直接访问。

1.3:类域

类定义了一个新的作用域——类域

对于新手(我)而言,代码一般是从上往下执行的按顺序执行的,为什么类中,最底下定义的成员能够在函数中被使用呢?

答:对于类而言,先检查类名;再检查类的成员;最后才是类的成员函数。

2:类的实例化

了解实例化前,需要知道一个概念:类的定义是不占用空间的,它就是一个房子的模型图,里面规定了各种各样的东西,只有当把他实例化了以后,实例化的对象才会占用一定空间,就像将房子的模型实际建造了出来。

同时一个类可以实例化出多个对象,就像一个房屋模型可以建造很多间房屋一样。

每个对象相互独立,没有任何关联。

class date
{
void House(int height, int area, int furniture)
{
_house_height = height;
_house_area = area;
_house_furniture_num = furniture;
}

void print()
{
std::cout << _house_height << std::endl;
std::cout << _house_area << std::endl;
std::cout << _house_furniture_num << std::endl;

}

int _house_height;
int _house_area;
int _house_furniture_num;
};

int main()
{
date h1;//房子1
date h2;//房子2

return 0;
}

2.1对象大小

实例化对象的大小包含类中所有成员变量,但不包含成员函数(成员函数会存储在一个单独的区域),如果一定要存储的话,那只能是成员函数的指针。

在知道了这一点后,对象的大小就和在学习C语言时所讲的sizeof()求结构体大小的对齐规则是一致的了,这里不再过多追叙。

注:我们知道对齐规则会导致系统空间的浪费,那么为什么一定需要浪费这一部分空间呢?不浪费不行吗?

:系统每次会以4个字节访问,倘若前一个为 char,后一个为 int,如果不按对其规则来,数据读取完毕后还得进行拼接操作,极其繁琐,其空间如图所示:

因此需要对齐操作来使得系统在读取数据时更为方便快捷。

3:this指针

以下代码为例:

class House
{
public:
void house(int height, int area, int furniture)
{
_house_height = height;
_house_area = area;
_house_furniture_num = furniture;
}

void print()
{
std::cout <<"房子高度:"<< _house_height << std::endl;
std::cout <<"占地面积:"<< _house_area << std::endl;
std::cout <<"家具数量:"<< _house_furniture_num << std::endl;

}
private:
int _house_height;
int _house_area;
int _house_furniture_num;
};

int main()
{
House h1;
House h2;

std::cout << "房子h1相关信息" << std::endl;
h1.house(20, 100, 30);
h1.print();

std::cout << std::endl;

std::cout << "房子h2相关信息" << std::endl;
h2.house(15, 150, 50);
h2.print();

return 0;
}

为了使类名 h1 、h2 正确调用函数,类的成员函数会默认在第一个位置增加一个当前类类型的指针可以理解为,在传参的过程中,把类的地址也作为实参传递给形参中,这样函数就能够根据对应的地址来进行后续操作。

注:

①:不能将this指针写进实参中,因为系统会默认传递。

②:在函数体内可以使用this指针

4:类的默认成员函数

定义:默认成员函数就是用户没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数。

我们需要带着以下两个疑问去学习默认构造函数:

①:我们不写时,编译器默认生成的函数行为是什么?是否满足我们的需求。

②:当编译器默认生成的函数不满足我们的需求,我们需要自己实现时,那么如何自己实现呢?

4.1:构造函数

非默认(一般)构造函数的编写:

class stack     //stack为类名
{
public:

    //构造函数
stack(int* arr , int capacity, int top )
{
_arr = arr;
_capacity = capacity;
_top = top;
}

private:
int* _arr;
int _capacity;
int _top;
};

int main()
{
stack st1(nullptr,4,0);//非默认构造函数,在实例化对象时,需要将参数传入

return 0;
}

根据上述代码可知构造函数具有以下特点:

①:函数名类名相同

②:无返回值

③:对于非默认构造函数而言,在实例化对象时需要传参,参数个数与缺省的参数数量有关。

默认构造函数有三个:在类中没有写构造函数、无参构造函数、全缺省构造函数。

//默认构造函数——不传参
class stack
{
public:

stack()
{
_arr = nullptr;
_capacity = 0;
_top = 0;
}

private:
int* _arr;
int _capacity;
int _top;
};

int main()
{
stack st1;

return 0;
}

//默认构造函数——全缺省

class stack
{
public:

stack(int* arr = nullptr, int capacity = 0, int top = 0)
{
_arr = arr;
_capacity = capacity;
_top = top;
}

private:
int* _arr;
int _capacity;
int _top;
};

int main()
{
stack st1;

return 0;
}

根据上述代码,可以得出构造函数的另外一个特点:

④:三种默认构造函数在类中只能存其一,不能同时存在。

///

在讲述最后一点特点前,需要知道一点,特别是对于初学者(我)而言。

什么是内置类型?

:整数类型(int,short,long)、字符类型char、浮点型float、指针类型等都是内置类型,编译器都认识他们,在编译时能够做出反应。

什么又是自定义类型?

:结构体(struct)、类(class)、枚举、联合等,这类都是自定义类型,编译器在编译过程中是不认识他们的,无法做出反应。

有了上述的了解后,再来认识构造函数的最后一个特点:

⑤:当我们不写默认构造函数时,编译器生成的默认构造函数,对内置类型成员变量的初始化没有要求,即初始化为何值取决于编译器。如下图:

对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化,如果该成员变量没有默认构造函数,那么就会报错。

其次,假设现存在类A(A中有构造函数),类B(B中的成员有 类A的实例化对象d),当我们在主函数创建 类B的实例化对象时,B中的 成员类A的实例化对象 d 为自动调用A中的构造函数进行初始化。如下图:

类queue中定义个两个自定义类stack,在创建实例化对象q时,会对其成员变量调用他们自己的构造函数,结果如下:

4.2:析构函数

析构函数的编写:

class stack
{
public:
stack(int n = 4)
{
_arr = (int*)malloc(sizeof(int) * n);
assert(_arr);
_capacity = n;
_top = 0;
}

    //析构函数
~stack()
{
free(_arr);
_arr = nullptr;
_capacity = 0;
_top = 0;
}

private:
int* _arr;
int _capacity;
int _top;
};

class queue
{
public:

private:
stack q1;
stack q2;
};

int main()
{
stack st;
queue q;

return 0;
}

析构函数具有以下特点:

①:析构函数名是在类名前加上 ~

②:无参数无返回值

③:一个类只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数

④:对象生命周期结束时,系统会自动调用析构函数

⑤:跟构造函数类似,我们不写时,编译器自动生成的析构函数对内置类型成员不做处理自定义类型会调用他的析构函数

⑥:还需要注意的是我们显示写析构函数,对于自定义类型成员也会调用他的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数

⑦:如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,如Date;如果默认生成的析构就可以⽤,也就不需要显示写析构,如MyQueue;但是有资源申请时,⼀定要自己写析构,否则会造成资源泄漏,如Stack

⑧:⼀个局部域的多个对象,C++规定后定义的先析构。

4.3:拷贝构造

拷贝构造用于一个对象拷贝初始化给另一个要创建的对象。
而赋值运算符用于完成两个已经存在的对象直接的拷贝赋值。

拷贝构造代码如下:


拷贝复制——浅拷贝//值拷贝

class date
{
public:
//构造函数
date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造  d是d1的别名  this 指针指向 d2
date(date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
    //也可以写成这种形式,对于新人来说方便理解
        this->_year = d._year;
this->_month = d._month;
this->_day = d._day;
}

void print()
{
std::cout << _year << "/" << _month << "/" << _day << std::endl;
}

private:
int _year;
int _month;
int _day;
};

int main()
{
date d1(2024, 10, 13);
date d2(d1);
    date d2 = d1; //这两种拷贝方式都可以
d2.print();
return 0;
}

根据上述代码,可以得出拷贝构造有如下特点:

①:拷贝构造时使用同类对象初始化创建对象,拷⻉构造函数的第⼀个参数必须是类类型对象的引⽤

②:C++规定自定义类型对象进行拷贝行为必须调用拷贝构造所以这⾥⾃定义类型传值传参和传值返回都会调⽤拷⻉构造完成。

③:若未显示定义拷贝构造,编译器会自动生成拷贝构造函数。⾃动生成的拷贝构造对内置类型成

员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷⻉),如上代码:
而对⾃定义类型成员变量会调⽤他的拷⻉构造,需要用到深拷贝,代码如下:
class stack
{
public:
stack(int n = 4)
{
_arr = (int*)malloc(sizeof(int) * n);
assert(_arr);
_capacity = n;
_top = 0;
}


    //深拷贝,需要开辟一块新的内存空间,将原来的值 copy到新的空间内。
stack(stack& st)
{
_arr = (int*)malloc(sizeof(int) * st._capacity);
assert(_arr);
memcpy(_arr, st._arr, sizeof(int) * st._top);
_capacity = st._capacity;
_top = st._top;
}


~stack()
{
free(_arr);
_arr = nullptr;
_capacity = 0;
_top = 0;
}

private:
int* _arr;
int _capacity;
int _top;
};

class queue
{
public:

private:
stack q1;
stack q2;
};

int main()
{
//stack st1;
//stack st2 = st1;

//queue q1;
//queue q2 = q1;

return 0;
}

问:为什么要用到深拷贝呢?

:如果采用之值拷贝/浅拷贝,对成员进行一个字节一个字节的拷贝,假设成员中包含资源调用(例如通过动态内存函数开辟的空间),那么定义两个实例化的对象后,这两个对象会指向同一块空间,这显然是不可取的,因此需要进行深拷贝。

④:拷贝构造与默认构造类似,当对象A中包含拷贝构造,对象B中包含成员A,那么将B拷贝构造给C时,B中的成员A会调用他自己的拷贝构造。

注:小贴士——类显示实现了析构并释放资源,那么他就需要显示写拷贝构造,否则就不需要。

⑤:传值返回会产生一个临时对象调用拷贝构造,而传引用返回,返回的是返回对象的别名,没有调用拷贝构造,能够提升效率。但是,如果返回对象是一个当前函数局部域的局部对象函数结束时就销毁了,那么引用时有问题的,这时的引用相当于一个野引用

:把局部函数的局部对象转换成全局变量就可以用传引用返回,例如用 static 去作用局部对象。

4.4:赋值运算符重载

赋值运算符用于完成两个已经存在的对象直接的拷贝赋值。

4.4.1:运算符重载

运算符重载有以下规定:
①:C++规 定类类型对象使⽤运算符时,必须转换成调⽤对应运算符重载,若没有对应的运算符重载,则会编译报错。

②:运算符重载是具有特殊名字的函数,他的名字是由operator和后⾯要定义的运算符共同构成。因此,运算符重载的代码如下:

date d1(2024, 9,19 );
date d2(1997, 5, 5);

//这两种都可以
d1.operator<(d2); 
d1 < d2;

③:运算符重载作为成员函数时,参数比运算对象少一个,对于二元运算符,左侧运算对象传给第一个参数,右侧运算对象传给第二个参数,因此上述代码中,operator< 函数的实现方式如下:

bool date::operator<(date& d)//d1传递给隐含的 this 指针 而d作为d2的别名
{
if (_year < d._year)
{
return true;
}
else if (_year == d._year && _month < d._month)
{
return true;
}
else if (_year == d._year && _month == d._month && _day < d._day)
{
return true;
}
return false;
}

4.4.2:赋值运算符重载

赋值运算符重载的实现代码:

date d1(2024, 9,19 );
date d2(1997, 5, 5);

date d3(d1);//这个是拷贝构造,⽤于⼀个对象拷⻉初始化给另⼀个要创建的对象

d1 = d2;//这个是赋值运算符重载,⽤于完成两个已经存在的对象直接的拷⻉赋值

可以通过调试证明,在调试过程中,dated3(d1)会调用拷贝构造,而 d1=d2不会。

赋值运算符重载的特点:

①:赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数。

②:有返回值,且建议写成当前类类型引⽤,引⽤返回可以提⾼效率,有返回值⽬的是为了⽀持连续赋值场景。注:注意是否存在野引用。

5:部分代码

date.h:

#pragma once
#include <iostream>
#include <assert.h>
#include <stdbool.h>

class date
{

public:

    //构造函数: 没有返回类型和返回值,与类名一致,参数与类成员尽量不一致
date(int year, int month, int day)
{
if (!checkdate(year, month, day))
{
std::cout << "日期输入有误,请重新输入" << std::endl;
}
_year = year;
_month = month;
_day = day;
}

//析构函数:同样没有返回类型和返回值,在类名前面加上 ~ ,
~date()
{
_year = 0;
_month = 0;
_day = 0;
}

//拷贝构造:同样没有返回类型和返回值,形参在同类的类名后加上& 表示拷贝构造
date(const date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}

void print();

bool operator<(date& d);

private:
int _year;
int _month;
int _day;
};

date.cpp:

#define  _CRT_SECURE_NO_WARNINGS
#include "date.h"

bool date::operator<(date& d)
{
if (_year < d._year)
{
return true;
}
else if (_year == d._year && _month < d._month)
{
return true;
}
else if (_year == d._year && _month == d._month && _day < d._day)
{
return true;
}
return false;
}


test.cpp

#define  _CRT_SECURE_NO_WARNINGS
#include "date.h"

int main()
{
date d1(2024, 9,19 );
date d2(1997, 5, 5);

date d3(d1);
d1 = d2;

return 0;
}


原文地址:https://blog.csdn.net/m0_51952310/article/details/142172635

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!