资讯专栏INFORMATION COLUMN

C++类和对象(万字总结)(建议收藏!!!)

masturbator / 2557人阅读

摘要:当你用该日期类创建一个对象时,编译器会自动调用该构造函数对新创建的变量进行初始化。注意构造函数的主要任务并不是开空间创建对象,而是初始化对象。编译器对内置类型使用默认构造函数时,对其成员赋的是随机值。

文章目录

面向过程和面向对象初步认识

C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。


类的引入

C语言中,结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。

struct Student{	void SetStudentInfo(const char* name, const char* gender, int age)	{		strcpy(_name, name);		strcpy(_gender, gender);		_age = age;	}	void PrintStudentInfo()	{		cout << _name << " " << _gender << " " << _age << endl;	}	char _name[20];	char _gender[3];	int _age;};

上面结构体的定义,在C++中更喜欢用class来代替


类的定义

class className{ // 类体:由成员函数和成员变量组成 }; // 一定要注意后面的分号

class为定义类的关键字ClassName为类的名字,{}中为类的主体注意类定义结束时后面分号
类中的元素称为类的成员:类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数

类的两种定义方法

1.声明和定义全部放在类体中

声明和定义全部放在类体中,需要注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处

class Student{	void SetStudentInfo(const char* name, const char* gender, int age)	{		strcpy(_name, name);		strcpy(_gender, gender);		_age = age;	}	void PrintStudentInfo()	{		cout << _name << " " << _gender << " " << _age << endl;	}	char _name[20];	char _gender[3];	int _age;};

2. 声明放在.h文件中,类的定义放在.cpp文件中

//person.hclass Person{public:   //显示信息   void show();public:   char* _name;   char* _sex;   int _age;}//person.cpp#include"person.h>void Person::show(){   cout<<_name<<"  "<<_sex<<"  "<<_age<<endl;}   

注意:一般情况下我们采用第二种方式


类的访问限定符及封装

访问限定符

C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其
接口提供给外部的用户使用。


【访问限定符说明】

  1. public修饰的成员在类外可以直接被访问
  2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
  3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
  4. class的默认访问权限为private,struct为public(因为struct要兼容C)

封装

在类和对象阶段,我们只研究类的封装特性,那什么是封装呢?

封装本质上是一种管理:我们使用类将数据和方法都封装起来。不想对外开放的就用 protected/private 封装起来,用 public 封装的成员允许外界对其进行合理的访问。所以封装本质上是一种管理。


类的作用域

类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员,需要使用 :: 作用域解析符指明成员属于哪个类域。

class Person{public: void PrintPersonInfo();private: char _name[20]; char _gender[3]; int _age;};// 这里需要指定PrintPersonInfo是属于Person这个类域void Person::PrintPersonInfo(){ cout<<_name<<" "_gender<<" "<<_age<<endl; }

类的实例化

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

  1. 类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它
  2. 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量
  3. 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什
    么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占
    用物理空间
class Person{public: void PrintPersonInfo();private: char _name[20]; char _gender[3]; int _age;};void test(){  Person man;   //类的实例化  man._name="hehe";  man._age="66";  man._sex="男";  man._PrintPersonInfo();}

类对象模型

如何计算类对象的大小

class A {public: void PrintA() { cout<<_a<<endl; }private: char _a;};

那么问题来了?类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大
小? 想要知道这个,首先我们要弄明白类在内存中的存储方式。

类对象的存储方式

那为什么内存要这样存储类了?
原因:每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多
个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。

结论:一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐,注意空类的大小,空类比
较特殊,编译器给了空类一个字节来唯一标识这个类。 如果有小伙伴不怎么明白内存对齐:可以看看这篇文章:自定义类型的知识点


this 指针

this指针的引出

我们先来定义一个日期类Date

class Date{ public : void Display () { cout <<_year<< "-" <<_month << "-"<< _day <<endl; }  void SetDate(int year , int month , int day) { _year = year; _month = month; _day = day; }private : int _year ; // 年 int _month ; // 月 int _day ; // 日};int main(){ Date d1, d2; d1.SetDate(2018,5,1); d2.SetDate(2018,7,1); d1.Display(); d2.Display(); return 0;  }

对于上述类,有这样的一个问题:
Date类中有SetDate与Display两个成员函数,函数体中没有关于不同对象的区分,那当d1调用SetDate函数
时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?

C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参
数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该
指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成

this指针的特性

  1. this指针的类型:类类型* const
  2. 只能在“成员函数”的内部使用
  3. this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针
  4. this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递

注意:this指针不能为空下面来看一个例子


这里为什么会报错了?首先这个p是一个空指针,但是并不是对象是空指针就一定报错,这里其实更重要的一个原因是PrintA里面为this->_a你对p进行了访问,而空指针是不能访问的。下面我们再来来p->Show()会不会报错?


类的6个默认成员函数

如果一个类中什么成员都没有,我们简称其为空类。但是空类中真的什么都没有吗?其实不然,任何一个类,即使我们什么都不写,类中也会自动生成6个默认成员函数。

class Date {}; //空类

构造函数

class Date{public:	Date(int year = 0, int month = 1, int day = 1)// 构造函数	{		_year = year;		_month = month;		_day = day;	}	void Print()	{		cout << _year << "年" << _month << "月" << _day << "日" << endl;	}private:	int _year;	int _month;	int _day;};

例如,上述日期类中的成员函数Date就是一个构造函数。当你用该日期类创建一个对象时,编译器会自动调用该构造函数对新创建的变量进行初始化。

注意:构造函数的主要任务并不是开空间创建对象,而是初始化对象。(这儿可以先暂时这么理解)

构造函数的特性

  1. 函数名与类名相同。
  2. 无返回值。
  3. 对象实例化时编译器自动调用对应的构造函数。
  4. 构造函数可以重载。
class Date{public:     // 1.无参构造函数     Date ()     {}	Date(int year = 0, int month = 1, int day = 1)// 构造函数	{		_year = year;		_month = month;		_day = day;	}	void Print()	{		cout << _year << "年" << _month << "月" << _day << "日" << endl;	}private:	int _year;	int _month;	int _day;};void TestDate(){ Date d1; // 调用无参构造函数 Date d2 (2015, 1, 1); // 调用带参的构造函数  // 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明 // 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象 Date d3(); }
  1. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
class Date{public: /* // 如果用户显式定义了构造函数,编译器将不再生成 Date (int year, int month, int day) { _year = year; _month = month; _day = day; } */private: int _year; int _month; int _day;};void Test(){  // 没有定义构造函数,对象也可以创建成功,因此此处调用的是编译器生成的默认构造函数   Date d; }
  1. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。
// 默认构造函数class Date{ public: Date() { _year = 1900 ; _month = 1 ; _day = 1; }  Date (int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; }private : int _year ; int _month ; int _day ;};// 以下测试函数能通过编译吗?void Test(){ Date d1; }


显然这儿是过不了的,因为类中有多个默认函数。

7.编译器对内置类型使用默认构造函数时,对其成员赋的是随机值。但对自定义类型,会调用它的默认函数。

这儿并没有我们自己写的构造函数,所以编译时会调用默认的构造函数,又由于类成员都是内置类型,因此赋的都是随机值。下面我们再来看看自定义类型。

注意:如果你Time类中没有自己写构造函数,用编译器默认的构造函数,它也是一样会输入随机值的。

析构函数

前面通过构造函数的学习,我们知道一个对象时怎么来的,那一个对象又是怎么没呢的?

析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而
对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。

特性

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数无返回值。
  3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
    编译器自动生成的析构函数机制:
     1、编译器自动生成的析构函数对内置类型不做处理。
     2、对于自定义类型,编译器会再去调用它们自己的默认析构函数。
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
  5. 先构造的后析构,后构造的先析构

拷贝构造函数

构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象 创建新对象时由编译器自动调用

特性

  1. 拷贝构造函数是构造函数的一个重载形式。
  2. 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
#include using namespace std;class Date{public:	Date(int year = 0, int month = 1, int day = 1)// 构造函数	{		_year = year;		_month = month;		_day = day;	}	Date(const Date& d)// 拷贝构造函数 ,与构造函数形成函数重载	{		_year = d._year;		_month = d._month;		_day = d._day;	}private:	int _year;	int _month;	int _day;};int main(){	Date d1(2021, 9, 27);	Date d2(d1); // 用已存在的对象d1创建对象d2	return 0;}


因此通过形参不写成引用的形式,会形成无限递归。

  1. 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷
    贝,这种拷贝我们叫做浅拷贝,或者值拷贝。

一般涉及到堆区的问题,浅拷贝是无法解决问题的。下面我们来举个例子:

class Stack{public:	Stack(int capacity = 4)	{		_ps = (int*)malloc(sizeof(int)* capacity);		_size = 0;		_capacity = capacity;	}	void Print()	{		cout << _ps << endl;// 打印栈空间地址	}private:	int* _ps;	int _size;	int _capacity;};int main(){	Stack s1;	s1.Print();// 打印s1栈空间的地址	Stack s2(s1);// 用已存在的对象s1创建对象s2	s2.Print();// 打印s2栈空间的地址	return 0;}

我们可以看到,类中没有自己定义拷贝构造函数,那么当我们用已存在的对象来创建另一个对象时,将调用编译器自动生成的拷贝构造函数。这段代码中,我们的本意是用已存在的对象s1创建对象s2,但编译器自动生成的拷贝构造函数,完成的是浅拷贝,拷贝出来的对象s2将不能满足我们的要求。

结果打印s1栈和s2栈空间的地址相同,这就意味着,就算在创建完s2栈后,我们对s1栈做的任何操作都会直接影响到s2栈。


这个时候问题就很严重了。首先我们对s1的修改都会直接影响s2,而且更重要的一个是:我们对它们共同指向的那块空间进行了两次的析构,会造成空间多次释放的问题。

运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类
型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

函数名字为:关键字operator后面接需要重载的运算符符号。

函数原型:返回值类型 operator操作符(参数列表)

注意
1.不能通过连接其他符号来创建新的操作符:比如operator@
2.重载操作符必须有一个类类型或者枚举类型的操作数
3.用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义
4.作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的
操作符有一个默认的形参this,限定为第一个形参
5.* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。

==运算符重载

bool operator==(const Date& d1, const Date& d2) { return d1._year == d2._year; && d1._month == d2._month && d1._day == d2._day; }

对于这个重载的函数,你可以定义再类里面,这样就少一个参数,因为有this指针的存在。你也可以定义在外面,但是定义在外面时,可能你的类成员时private封装的,无法访问到,这时有两个解决办法:一是把类成员用public封装,二是用友元函数(之后会讲到)。

= 运算符重载

	Date& operator=(const Date& d)// 赋值运算符重载函数	{		if (this != &d)		{			_year = d._year;			_month = d._month;			_day = d._day;		}		return *this;	}

这里为什么要返回引用了?如果你去测试发现D1=D2,如果你的返回值是Date的话,似乎也能过,但是如果你的测试用例是D1=D2=D3的话,那就一定过不了了,因为你不是返回的对象本身,无法形成链式编程,这也是为什么这儿返回*this的原因,因为this是指向左操作符的。

其他一些运算符的重载这儿就不多说了,有兴趣的小伙伴可以自己去尝试尝试。下面来说几个重载运算符时的注意点。

重载赋值运算符需要注意以下几点
、参数类型设置为引用,并用const进行修饰
赋值运算符重载函数的第一个形参默认是this指针,第二个形参是我们赋值运算符的右操作数。
由于是自定义类型传参,我们若是使用传值传参,会额外调用一次拷贝构造函数,所以函数的第二个参数最好使用引用传参(第一个参数是默认的this指针,我们管不了)。
其次,第二个参数,即赋值运算符的右操作数,我们在函数体内不会对其进行修改,所以最好加上const进行修饰。

、返回值使用引用返回
原因在=运算符重载中说过了,为了返回对象自身,形成链式编程。(return *this才是返回自身,不要忘记解引用哦)

、一个类如果没有显示定义赋值运算符重载,编译器也会自动生成一个,完成对象按字节序的值拷贝

没错,赋值运算符重载编译器也可以自动生成,并且也是支持连续赋值的。但是编译器自动生成的赋值运算符重载完成的是对象按字节序的值拷贝,例如d2 = d1,编译器会将d1所占内存空间的值完完全全地拷贝到d2的内存空间中去,类似于memcpy。
但是有些类就不行了,所以有些类还是要我们自己写赋值运算符重载的。

注意区分拷贝和赋值:

	Date d1(2021, 6, 1);	Date d2(d1);	Date d3 = d1;

这里一个三句代码,我们现在都知道第二句代码调用的是拷贝构造函数,那么第三句代码呢?调用的是哪一个函数?是赋值运算符重载函数吗?
其实第三句代码调用的也是拷贝构造函数,注意区分拷贝构造函数和赋值运算符重载函数的使用场景:
拷贝构造函数:用一个已经存在的对象去构造初始化另一个即将创建的对象。
赋值运算符重载函数:在两个对象都已经存在的情况下,将一个对象赋值给另一个对象。


const修饰成员函数

我们将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰的是类成员函数隐含的this指针,表明在该成员函数中不能对this指针指向的对象进行修改。

例如,我们可以对类成员函数中的打印函数进行const修饰,避免在函数体内不小心修改了对象:

	void Print()const// cosnt修饰的打印函数	{		cout << _year << "年" << _month << "月" << _day << "日" << endl;	}

注意:在使用const时要注意,权限不能放大,但是可以缩小。


再谈构造函数

构造函数体赋值

在创建对象时,编译器会通过调用构造函数,给对象中的各个成员变量一个合适的初始值:

class Date{public:	// 构造函数	           
               
                                           
                       
                 

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/121415.html

相关文章

  • ❤️爆肝十二万字《python从零到精通教程》,从零教你变大佬❤️(建议收藏

    文章目录 强烈推荐系列教程,建议学起来!! 一.pycharm下载安装二.python下载安装三.pycharm上配置python四.配置镜像源让你下载嗖嗖的快4.1pycharm内部配置 4.2手动添加镜像源4.3永久配置镜像源 五.插件安装(比如汉化?)5.1自动补码神器第一款5.2汉化pycharm5.3其它插件 六.美女背景七.自定义脚本开头八、这个前言一定要看九、pyt...

    booster 评论0 收藏0
  • 一个脚本教你快速去除桌面图标烦人的小箭头

    摘要:于是乎,冰河写了一个脚本完美去除了桌面图标烦人的小箭头。今天,给大家分享一个如何完美去除桌面快捷图标小箭头的技巧,希望能够给大家带来帮助。这种方法不会导致任何问题可放心使用,冰河已经亲自测试过了。 ...

    Jaden 评论0 收藏0
  • ❤️❤️新生代农民工爆肝8万字,整理Python编程从入门到实践(建议收藏)已码:6万字❤️❤️

    人生苦短,我用Python 开发环境搭建安装 Python验证是否安装成功安装Pycharm配置pycharm 编码规范基本语法规则保留字单行注释多行注释行与缩进多行语句数据类型空行等待用户输入print输出 运算符算术运算符逻辑运算符成员运算符身份运算符运算符优先级 字符串访问字符串中的值字符串更新合并连接字符串删除空白startswith()方法endswith()方法字符串格式化...

    wthee 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<