摘要:当你用该日期类创建一个对象时,编译器会自动调用该构造函数对新创建的变量进行初始化。注意构造函数的主要任务并不是开空间创建对象,而是初始化对象。编译器对内置类型使用默认构造函数时,对其成员赋的是随机值。
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为类的名字,{}中为类的主体,注意类定义结束时后面分号。
类中的元素称为类的成员:类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数。
声明和定义全部放在类体中,需要注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处
理
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;};
//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++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其
接口提供给外部的用户使用。
【访问限定符说明】
在类和对象阶段,我们只研究类的封装特性,那什么是封装呢?
封装本质上是一种管理:我们使用类将数据和方法都封装起来。不想对外开放的就用 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; }
用类类型创建对象的过程,称为类的实例化
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;};
那么问题来了?类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大
小? 想要知道这个,首先我们要弄明白类在内存中的存储方式。
那为什么内存要这样存储类了?
原因:每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多
个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。
结论:一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐,注意空类的大小,空类比
较特殊,编译器给了空类一个字节来唯一标识这个类。 如果有小伙伴不怎么明白内存对齐:可以看看这篇文章:自定义类型的知识点
我们先来定义一个日期类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指针不能为空
下面来看一个例子
这里为什么会报错了?首先这个p是一个空指针,但是并不是对象是空指针就一定报错,这里其实更重要的一个原因是PrintA里面为this->_a你对p进行了访问,而空指针是不能访问的。下面我们再来来p->Show()会不会报错?
如果一个类中什么成员都没有,我们简称其为空类。但是空类中真的什么都没有吗?其实不然,任何一个类,即使我们什么都不写,类中也会自动生成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就是一个构造函数。当你用该日期类创建一个对象时,编译器会自动调用该构造函数对新创建的变量进行初始化。
注意:构造函数的主要任务并不是开空间创建对象,而是初始化对象。(这儿可以先暂时这么理解)
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(); }
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; }
// 默认构造函数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类中没有自己写构造函数,用编译器默认的构造函数,它也是一样会输入随机值的。
前面通过构造函数的学习,我们知道一个对象时怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而
对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象 创建新对象时由编译器自动调用
#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;}
因此通过形参不写成引用的形式,会形成无限递归。
一般涉及到堆区的问题,浅拷贝是无法解决问题的。
下面我们来举个例子:
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修饰类成员函数,实际修饰的是类成员函数隐含的this指针,表明在该成员函数中不能对this指针指向的对象进行修改。
例如,我们可以对类成员函数中的打印函数进行const修饰,避免在函数体内不小心修改了对象:
void Print()const// cosnt修饰的打印函数 { cout << _year << "年" << _month << "月" << _day << "日" << endl; }
注意:
在使用const时要注意,权限不能放大,但是可以缩小。
在创建对象时,编译器会通过调用构造函数,给对象中的各个成员变量一个合适的初始值:
class Date{public: // 构造函数
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/121415.html
文章目录 强烈推荐系列教程,建议学起来!! 一.pycharm下载安装二.python下载安装三.pycharm上配置python四.配置镜像源让你下载嗖嗖的快4.1pycharm内部配置 4.2手动添加镜像源4.3永久配置镜像源 五.插件安装(比如汉化?)5.1自动补码神器第一款5.2汉化pycharm5.3其它插件 六.美女背景七.自定义脚本开头八、这个前言一定要看九、pyt...
摘要:于是乎,冰河写了一个脚本完美去除了桌面图标烦人的小箭头。今天,给大家分享一个如何完美去除桌面快捷图标小箭头的技巧,希望能够给大家带来帮助。这种方法不会导致任何问题可放心使用,冰河已经亲自测试过了。 ...
人生苦短,我用Python 开发环境搭建安装 Python验证是否安装成功安装Pycharm配置pycharm 编码规范基本语法规则保留字单行注释多行注释行与缩进多行语句数据类型空行等待用户输入print输出 运算符算术运算符逻辑运算符成员运算符身份运算符运算符优先级 字符串访问字符串中的值字符串更新合并连接字符串删除空白startswith()方法endswith()方法字符串格式化...
阅读 2940·2021-10-28 09:32
阅读 2965·2021-10-11 10:57
阅读 3110·2021-10-08 10:05
阅读 2586·2021-09-28 09:36
阅读 2210·2019-08-30 15:55
阅读 2269·2019-08-30 15:44
阅读 2391·2019-08-30 14:02
阅读 3073·2019-08-29 17:16