欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 科技 > IT业 > 类和对象(上)

类和对象(上)

2025/5/16 2:23:01 来源:https://blog.csdn.net/2302_79981885/article/details/140369315  浏览:    关键词:类和对象(上)

前言

类和对象(上)

1.类初始介绍

以前C语言中结构体中就只能引入变量,而现在C++中结构体中就可以引入函数了

typedef int DataType;
struct Stack
{void init(){a = NULL;capacity = 0;size = 0;}int* a;int capacity;int size;
};

但现在一般不用struct了,一般用class,这两个是一样的作用

typedef int DataType;
struct Stack
{void init(){a = NULL;capacity = 0;size = 0;}int* a;int capacity;int size;
};

这个Stack就是一个类,用类来创造的变量叫做对象,而且创造对象时不用写class,直接写Stack
在这里插入图片描述
这个Stack就是类,s1就是对象
函数调用如上
类里面的函数叫做成员函数,变量叫做成员变量

2.访问限定符

类里面有三个,public(公开可以访问),private(私有不可访问),和protected其中protected我们暂时不用
在这里插入图片描述
class默认是private,struct默认是public,所以这里会报错

class Stack
{
public:void init(){a = NULL;capacity = 0;size = 0;}
private:int* a;int capacity;int size;
};
int main()
{Stack s1;s1.init();return 0;
}

一个访问限定符的范围是到另一个访问限定符,如果没有另一个,就直接到末尾,没有说明是什么访问限定符,就是用系统默认的访问限定符
一般成员变量用private,成员函数用public,为了防止用户修改数据嘛

3.成员变量的命名规则

class Date
{
public:void Init(int year){// 这里的year到底是成员变量,还是函数形参?year = year;}void print(){cout << year << endl;}
private:int year;
};int main()
{Date d;d.Init(2);d.print();
}

在这里插入图片描述
有些时候会出现这种情况,就是不知道year到底是什么,其实是局部变量,因为局部变量优先嘛,所以没有修改对象的year,所以才有了命名规则

class Date
{
public:void Init(int year){// 这里的year到底是成员变量,还是函数形参?_year = year;}void print(){cout << _year << endl;}
private:int _year;
};int main()
{Date d;d.Init(2);d.print();
}

在这里插入图片描述

4.成员函数的声明与定义的分开

在这里插入图片描述
在这里插入图片描述
如果就是这样写的话,是无法访问到_year的,因为_year相当于在一个作用域里面,所以要写好哪个地方的_year
在这里插入图片描述
这样就可以了

6.类的实例化

类本身不占空间,类实例化出来的对象才占空间
然后就是类的大小的计算,类的大小就是成员变量的大小,而成员函数的话是存放在公共的区域(公共代码区)里的,如果每个对象都存放成员函数,那么浪费的空间太多了
而成员变量的大小就是以前算结构体大小的方法,就是结构体的内存对齐规则

class Date
{
public:void Init(int year);void print();
private:int _year;int _month;char a;
};

在这里插入图片描述
结构体的内存对齐规则:

  1. 第一个成员在与结构体偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
    注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
    VS中默认的对齐数为8
  3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
    体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
// 类中仅有成员函数
class A2 {
public:void f2() {}
};
// 类中什么都没有---空类
class A3
{};

注意这两个的话的大小是一个字节,理论上应该是没有字节大小的,但是可能吗,你创建一个遍变量还没有大小,这是不可能的,所以会使用一个字节来说明创建了变量,但是不会在里面放数据

7.this指针

我们先定义一个日期类

class Date
{
public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}void print(){cout << _year << '-' << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;
};int main()
{Date a;a.Init(2024, 7, 12);a.print();return 0;
}

在这里插入图片描述
这里我们提个疑问,为什么调用公共代码区的函数就能改变对应对象的成员变量了呢
原因是你在使用 a.print();的时候,其实是传入了a的地址的了,就是这样的 a.print(&a);
然后对应函数的参数会用一个this指针来接收地址,就是这样


void print(Date*this)
{cout << _year << '-' << _month << "-" << _day << endl;
}

然后对应函数的实现就会变成这样

	void print(Date*this){cout << this->_year << '-' << this->_month << "-" << this->_day << endl;}

但这些步骤都是编译器自己实现的,根本不用你写
而且你写了取地址,和在临时参数那里写了this指针的话还会报错,只有在函数里面写了this才不会有错
在这里插入图片描述
而且this指针是const类型的
this指针是形参所以一般会存在栈区,不会存在对象中,不会占据对象空间,有些时候它还会存在寄存器中

// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A {
public:void Print(){cout << "Print()" << endl;}
private:int _a;
};
int main()
{A* p = nullptr;p->Print();return 0; }

这个程序会正常运行, p->Print();这个看似好像对空指针进行了解引用,其实嘛,这只是个调用函数而已,调用函数之前要传参,我们说过,这个调用函数其实就是传地址罢了,p->说明是传p的地址,穿的就是nullptr空指针,传空指针肯定不会报错啊,所以没问题

// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{ 
public:void PrintA() {cout<<_a<<endl;}
private:int _a;
};
int main()
{A* p = nullptr;p->PrintA();return 0; }

这个就有问题了,因为this指针是空指针,所以cout<<_a<<endl;这一步的实际操作是 cout<_a<<endl;对空指针解引用了,虽然不会报错,因为对空指针解引用是不会报错的,只会运行崩溃

8.构造函数

编译器有六个默认成员函数
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
这里我们先讲一个构造函数
构造函数的作用就相当于init的作用
1.构造函数函数名与类名相同
2.它没有返回值,甚至连void都不需要写,如果你写了void就不是构造函数了

class Date
{
public:Date(){_year = 1;_month = 1;_day = 1;}void print(){cout << this->_year << '-' << this->_month << "-" << this->_day << endl;}
private:int _year;int _month;int _day;
};int main()
{Date a;a.print();return 0;
}

构造函数的使用时间是在你定义对象的时候系统会自动使用了,没有参数的构造函数就是这样使用的,定义的时候就使用了

class Date
{
public:Date(int year,int month,int day){_year = year;_month = month;_day = day;}void print(){cout << this->_year << '-' << this->_month << "-" << this->_day << endl;}
private:int _year;int _month;int _day;
};int main()
{Date a(1,2,3);a.print();return 0;
}

有参数的构造函数是这样使用的
3.构造函数是允许重载的,意思是可以定义多个构造函数

class Date
{
public:Date(int year,int month,int day){_year = year;_month = month;_day = day;}Date(){_year = 1;_month = 1;_day = 1;}void print(){cout << this->_year << '-' << this->_month << "-" << this->_day << endl;}
private:int _year;int _month;int _day;
};

你怎么使用就决定了使用哪个构造函数
Date a();无参的构造函数不能这样使用,因为这样无法与函数的声明区分开,就只能这样用Date a;

class Date
{
public:Date(int year=1,int month=2,int day=3){_year = year;_month = month;_day = day;}Date(){_year = 1;_month = 1;_day = 1;}void print(){cout << this->_year << '-' << this->_month << "-" << this->_day << endl;}
private:int _year;int _month;int _day;
};int main()
{Date a;a.print();return 0;
}

这里系统会调用无参的构造函数,就会有歧义,不知道调用哪个
上述我们手动写出来了构造函数,这个构造函数叫做显示定义构造函数
如果我们没有手动生成显示定义构造函数,那么编译器就会自动生成无参的默认构造函数
这个无参的默认构造函数也是在定义变量的时候就会调用的
它是怎么实现的呢

class stack
{
public:stack(){cout << "	stack()" << endl;}
private:int a;
};class Date
{
public:void print(){cout << this->_year << '-' << this->_month << "-" << this->_day << endl;}
private:int _year;int _month;int _day;stack a;
};int main()
{Date a;a.print();return 0;
}

在这里插入图片描述
编译器自动生成无参的默认构造函数对于内置类型(基本类型)(也是int char 这种类型)没有处理,有的编译器可能是处理的随机值,有的编译器可能处理的是0,VS处理的就是随机值,而对于自定义类型(也就是结构体 类这种)默认构造函数对于他们的处理就是调用他们的构造函数,如果还没有,就又是系统自动生成的函数就可以了

内置类型成员变量在声明中可以给变量默认值,相当于缺省参数那种用法

class Date
{
public:void print(){cout << this->_year << '-' << this->_month << "-" << this->_day << endl;}
private:int _year=16;int _month=12;int _day=10;
};int main()
{Date a;a.print();return 0;
}

在这里插入图片描述
如果成员变量内置类型有默认值,那么在定义对象的时候就会最先去走默认值这一步,先去把year month day 给赋值了再说,然后又是因为是默认构造函数,所以year这些变量有了值就是这些值了,不会改变了

class Date
{
public:Date(){_year = 28547;_month = 234;_day = 24;}void print(){cout << this->_year << '-' << this->_month << "-" << this->_day << endl;}
private:int _year=16;int _month=12;int _day=10;
};int main()
{Date a;a.print();return 0;
}

在这里插入图片描述
已经说过了,要先去走默认值,再去走构造函数,所以结果是这样的,这两步都是系统自己干的

无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为
是默认构造函数。这些都是可以不用写参数就能调用的构造函数

一般建议要写构造函数,而且是全缺省的构造函数,还要写内置类型的成员变量的默认值

8.析构函数

所谓析构函数,的作用就相当于destroy的作用而已

1.析构函数的名字是类名前面加上~
2.析构函数也没有返回值,也不需要写void,会在对象的生命周期结束后自动调用,析构函数也没有参数

class stack
{
public:stack(int m=4){_a = (int*)malloc(sizeof(int) * m);_capacity = 0;_size = 0;}void print(){/}~stack(){cout << "~stack()" << endl;if (_a){free(_a);_a = NULL;_capacity = 0;_size = 0;}}
private:int* _a;int _capacity;int _size;
};int main()
{stack a;return 0;
}

在这里插入图片描述
3.注意析构函数只有一个,不能重载
4.如果析构函数我们没有定义,没有显示定义,那么系统会自动生成默认的析构函数
而这个系统会自动生成默认的析构函数和构造函数差不多,对于内置类型不管不处理,对于自定义类型就会调用它的析构函数
所以这样系统默认的析构函数就会有问题,对于指针这个内置类型就不会处理,就不会释放空间
所以析构函数还是要自己写才行

9.拷贝构造函数

所谓拷贝构造函数就是在初始化一个对象的时候,直接复制另一个对象的内容的这个函数
这个还是构造函数,是构造函数的重载形式
拷贝构造函数的形参必须只有一个,而且是另一个对象的引用

class Date
{
public:Date(){_year = 28547;_month = 234;_day = 24;}Date(Date& a){_year = a._year;_month = a._month;_day = a._day;}void print(){cout << this->_year << '-' << this->_month << "-" << this->_day << endl;}
private:int _year = 16;int _month = 12;int _day = 10;
};int main()
{Date a;Date b(a);b.print();return 0;
}
Date b(a);就这样还是在创建变量的时候就调用构造函数,加个(a)就说明用的是拷贝构造
int main()
{Date a;Date b=a;b.print();return 0;
}

有些时候也这样写,都一样的,都是拷贝构造,但这个也反映了一个事情,自定义类型的赋值会调用拷贝构造
拷贝构造的形参必须是对象的引用,为什么不能直接写Date a呢
在这之前,我们先弄明白一个事情

class Date
{
public:Date(){_year = 28547;_month = 234;_day = 24;}Date(Date& a){cout << "Date(Date& a)" << endl;_year = a._year;_month = a._month;_day = a._day;}void print(){cout << this->_year << '-' << this->_month << "-" << this->_day << endl;}
private:int _year = 16;int _month = 12;int _day = 10;
};void push(Date b)
{cout << "void push(Date b)" << endl;
}int main()
{Date a;push(a);return 0;
}

在这里插入图片描述
这个例子的结果也说明自定义类型的赋值会调用拷贝构造
回到前面,如果是这样写的拷贝构造

	Date(Date a){cout << "Date(Date& a)" << endl;_year = a._year;_month = a._month;_day = a._day;}
	Date a;Date b(a);

拷贝构造函数在调用之前会先进行参数的赋值,参数的赋值就会调用拷贝构造函数,调用这个拷贝构造函数又会这样,就这样就会一直死递归,所以不行
为什么不取地址来拷贝构造呢,因为就是这样规定的,直接传a就可以了,不传&a

若未显式定义,编译器会生成默认的拷贝构造函数,编译器会生成默认的拷贝构造函数就是直接拷贝那个对象的数据,按字节一个一个拷贝

而这个编译器会生成默认的拷贝构造函数也会出现一些问题,就是在拷贝指针开辟的空间的的时候,就只是单纯的拷贝指针,导致两个对象的指针指向同一块空间,那么这样就会free两次,就会出问题
而正确的操作应该是我们拷贝指针对应的空间,所以开辟空间的拷贝构造函数还是要自己实现的好,如果没有开辟空间,可以自己不实现

然后就是内置类型是按字节拷贝,自定义类型就是按照它的拷贝构造函数去实现的

下面我们来实现一个栈的拷贝构造

class stack
{
public:stack(int m = 4){_a = (int*)malloc(sizeof(int) * m);_capacity = 0;_size = 0;}stack(stack&s){_a = (int*)malloc(sizeof(int) * s._capacity);memcpy(_a, s._a, sizeof(int) * s._size);_capacity = s._capacity;_size = s._size;}void print(){/}~stack(){cout << "~stack()" << endl;if (_a){free(_a);_a = NULL;_capacity = 0;_size = 0;}}
private:int* _a;int _capacity;int _size;
};

这里我们就可以看出来了,只要有开辟空间的拷贝构造就要自己实现了

总结

这一节都到此结束了,下一篇博客接着写类和对象

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词