一、构造函数
1.构造函数特点
- 与类同名
- 没有返回值
- 创建对象的时候执行
- 主要用于初始化对象(想干其他事也行,自己在构造函数中定义)
- 一个类中可以有多个构造函数(最好有一个无参的构造函数),多个构造函数之间构成重载关系
- 编译器不要求类中必须有构造函数,如果自己没有定义构造函数,编译器会默认使用一个空的构造函数
- 子类创建对象时,编译器会先调用其父类的构造函数(如果继承了多层父类,则会最先调用最老的祖宗的构造函数),再调用子类的构造函数
2.构造函数举例
-
代码如下:
#include "stdafx.h" struct Person{ int age; int level; Person(){ //空的构造函数 } Person(int age,int level){ //初始化成员变量的构造函数 this->age = age; this->level = level; } void Print() { printf("%d-%d\n",age,level); } }; void Test(){ //测试函数 Person p(18,1); //创建对象时调用初始化成员变量的构造函数 Person p2; //创建对象时使用空的构造函数 p.Print(); //18-1 p2.Print(); //垃圾数据-垃圾数据 } int main(int argc, char* argv[]){ Test(); return 0; }
3.函数重载
-
函数的重载:当有多个同名函数,但是函数的参数个数、类型不同时,多个函数间就构成了重载的关系,即编译器会根据你调用函数时给的参数来判断应该调用哪一个同名函数
-
比如下面的例子:Person类中有3个同名的函数,但是参数的个数和类型不尽相同,所以当调用method成员方法时,编译器会根据传入的参数类型或者个数来判断,究竟应该调用哪一个同名成员方法
#include "stdafx.h" struct Person{ void method(int x){ printf("1"); } void method(char x){ printf("2"); } void method(int x,int y){ printf("3"); } }; void Test(){ //测试函数 Person p; char x = 1; int y = 1; p.method(x); //2 p.method(y); //1 p.method('a'); //2 p.method(47); //1 p.method(1,y); //3 } int main(int argc, char* argv[]){ Test(); return 0; }
二、析构函数
1.析构函数特点
-
一个类只能有一个析构函数,不能重载
因为析构函数在销毁对象时由编译器(系统)自动调用,你没法指明调用哪个,主要用于清理工作,所以你如果定义多个同名析构函数,一个是没有必要,第二个是编译器或系统不知道调用哪个了
-
不能带任何参数和返回值
-
主要用于清理工作(也可以在析构函数中加一些自己想要的功能)
-
编译器不要求必须提供
2.析构函数举例
-
对于普通的成员变量来说,都是在栈中分配空间存储,当函数执行结束就会成为垃圾数据;但是如果创建对象时申请了堆空间,那么在销毁对象时就必须要释放堆,所以就需要析构函数出场,做清理工作
-
代码如下:当test()测试函数结束后,对象就没了,所以编译器(系统)就会调用析构函数,把堆释放掉
#include "stdafx.h" #include <malloc.h> struct Person{ int age; int level; char* arr; Person(){ } Person(int age,int level){ printf("有参构造函数执行了\n"); this->age = age; this->level = level; arr = (char*)malloc(1024); //申请堆 } ~Person(){ //析构函数 printf("析构函数执行了\n"); free(arr); //释放堆 arr = NULL; } }; void Test(){ //测试函数 Person p(18,1); } int main(int argc, char* argv[]){ Test(); return 0; }
三、继承
1.什么是继承
-
继承的本质就是数据的复制
-
通过反汇编来验证:先定义一个Person类和Student类,Student类中有两个和Person类中的变量是同名的,还有两个是自己独有的,给Student类的对象stu赋值,查看反汇编
#include "stdafx.h" struct Person{ int age; int sex; }; struct Student{ int age; int sex; int code; int score; }; void Test(){ //测试函数 Student stu; stu.age = 1; stu.sex = 2; stu.code = 3; stu.score = 4; printf("%d\n",sizeof(stu)); //16 } int main(int argc, char* argv[]){ Test(); return 0; }
反汇编如下:发现成员变量存储地址依次为[ebp-0x10]、[ebp-0xC]、[ebp-8]、[ebp-4]
-
再定义一个Person类和Student类,只不过Student类中的由于也有age和sex属性,所以可以直接继承Person类,再定义自己的属性code和score
#include "stdafx.h" struct Person{ int age; int sex; }; struct Student:Person{ //Student类继承Person类 int code; int score; }; void Test(){ //测试函数 Student stu; stu.age = 1; stu.sex = 2; stu.code = 3; stu.score = 4; printf("%d\n",sizeof(stu)); //16 } int main(int argc, char* argv[]){ Test(); return 0; }
反汇编如下:会发现即使Student类中没有定义age和sex属性,但是由于继承了Person类,那么就会把Person类中的age和sex两个属性复制过来!即成员变量存储地址还是依次为[ebp-0x10]、[ebp-0xC]、[ebp-8]、[ebp-4];且Student类的对象大小还是16字节
-
综上为什么要继承:减少重复代码的编写
-
Person称为父类;Student称为子类
2.父类指针指向子类的对象
-
可以用类指针(结构体指针)访问成员变量:
#include "stdafx.h" struct Person{ int age; int sex; }; struct Student:Person{ //Student类继承Person类 int code; int score; Student(){ //构造函数初始化 this->age = 1; this->sex = 2; this->code = 3; this->score = 4; } }; void Test(){ Student stu; //创建Student类的对象 Student* p = &stu; //定义Student类的指针,指向stu对象首地址 printf("%d %d\n",p->age,p->code); //可以用类指针访问成员变量1 3 } int main(int argc, char* argv[]){ Test(); return 0; }
-
那如果使用父类指针,能否访问子类中的成员变量呢?可以的:
Person* p = &stu;
。表示定义一个Person类的结构体指针,指向stu对象的首地址!那么可以用p->age
和p->sex
来访问对象的age和sex属性。#include "stdafx.h" struct Person{ int age; int sex; }; struct Student:Person{ //Student类继承Person类 int code; int score; Student(){ this->age = 1; this->sex = 2; this->code = 3; this->score = 4; } }; void Test(){ Student stu; //创建Student类的对象 Person* p = &stu; //父类指针指向子类的对象 printf("%d %d\n",p->age,p->sex); //1 2 } int main(int argc, char* argv[]){ Test(); return 0; }
-
但是不要忘了结构体指针访问数据的原理:(原理是根据宽度从结构体起始地址偏移,而不是根据变量的名字来查找的!!!)
p->age
表示从p指针指向的地址开始,向后访问age变量大小个字节的数据,所以能访问到1;p->sex
表示p指针指向的地址向后偏移4字节后,再访问sex变量大小个字节的数据,所以能访问到2。虽然Student类中往后还有code和score两个属性,但是由于你定义的是Person结构体指针,编译器不认识->code
和->score
,所以自然不能p->code
。 -
但是学了这么久的指针了,我们想用父类指针访问Student类中code和score也不难,只需要p++即可
#include "stdafx.h" struct Person{ int age; int sex; }; struct Student:Person{ //Student类继承Person类 int code; int score; Student(){ this->age = 1; this->sex = 2; this->code = 3; this->score = 4; } }; void Test(){ Student stu; //创建Student类的对象 Person* p = &stu; //父类指针指向子类的对象 p++; //p指针指向的地址向后偏移8字节,即现在指向了[ebp-8],也就是code的位置 printf("%d %d\n",p->age,p->sex); //3 4 } int main(int argc, char* argv[]){ Test(); return 0; }
-
-
那既然父类指针可以指向子类的对象,那子类指针能否指向父类的对象呢?语法上是不允许的,但是我们学底层,强转就可以了,但是就可以访问到不属于父类范围内的数据,不安全,所以不建议。比如:
#include "stdafx.h" struct Person{ int age; int sex; Person(){ this->age = 1; this->sex = 2; } }; struct Student:Person{ //Student类继承Person类 int code; int score; }; void Test(){ Person person; //创建Student类的对象 Student* p = (Student*)&person; //子类指针指向父类的对象 printf("%d %d %d %d\n",p->age,p->sex,p->code,p->score); //1 2 未知数据 未知数据 } int main(int argc, char* argv[]){ Test(); return 0; }
p->age
和p->sex
都可以访问到person对象的age和sex成员,但是p->code
和p->score
继续向后偏移访问,后面的空间就不属于person对象了,是其他数据了。所以子类指针访问的范围比父类的范围大,就会不安全
3.多层继承
-
就是Z继承Y,Y继承X,那么Z类对象大小为12,Y类对象大小为8,X类对象大小为4。(很简单就不细说了)
#include "stdafx.h" struct X{ int a; }; struct Y:X{ int b; }; struct Z:Y{ int c; }; void Test(){ Z z; z.a = 1; z.b = 2; z.c = 3; Y* py = &z; //父类指针指向子类对象 X* px = &z; //爷类指针指向子类对象 printf("%d %d %d %d %d %d\n",py->a,py->b,px->a,z.a,z.b,z.c); //1 2 1 1 2 3 } int main(int argc, char* argv[]){ Test(); return 0; }
4.子类和父类成员变量同名
-
如果出现子类和父类成员变量同名,情况如下:
#include "stdafx.h" struct X{ int a; int b; }; struct Y:X{ int a; int c; }; struct Z:Y{ int d; }; void Test(){ Z z; z.d = 1; z.a = 2; //默认会从自己开始逐级往父类找,先找Z,再找Y,再找X...所以这里的a其实是Y的a z.Y::a = 3; //这里指明是Y类的a z.X::a = 4; //这里指明是X类的a z.b = 5; z.c = 6; printf("%d\n",sizeof(z)); } int main(int argc, char* argv[]){ Test(); return 0; }
-
即使Y和X中有同名成员变量,但是Z继承Y,Y继承X,**Z的大小还是5 * 4 = 20字节大小!**因为对于编译器来说,变量定义成什么名字它不管,它只认这是一个4字节大小的数据,给每个数据分配空间。
-
但是我们写代码的时候要告诉编译器我们想让他找的是哪个类里面的a,所以就必须指明:
z.X::a
,这个表示X类中的a;z.Y::a
这个表示Y类中的a
-
5.多重继承
-
一个类有多个父类,但多重继承增加了程序的复杂度,容易出错,微软建议使用单继承,如果需要多重继承可以改为多层继承
#include "stdafx.h" struct X{ int a; int b; }; struct Y{ int a; int d; }; struct Z:X,Y{ //多重继承 int e; int f; }; void Test(){ Z z; z.e = 1; z.f = 2; z.Y::a = 3; //这里指明是Y类的a z.X::a = 4; //这里指明是X类的a z.b = 5; z.d = 6; printf("%d\n",sizeof(z)); } int main(int argc, char* argv[]){ Test(); return 0; }
多重继承和多层继承的区别就在于,如图所示:
四、作业
1.设计一个类DateInfo
-
设计一个结构DateInfo,要求其满足下述要求:
- 有三个成员: int year; int month;int day;
- 要求有个带参数的构造函数,其参数分别为对应年、月、日。
- 有一个无参数的构造函数,其初始的年、月、日分别为:2015、4、2。
- 要求有一个成员函数实现日期的设置:SetDay(int day)
- 要求有一个成员函数实现日期的获取: GetDay()
- 要求有一个成员函数实现年份的设置: SetYear(int year)
- 要求有一个成员函数实现年份的获取: GetYear()
- 要求有一个成员函数实现月份的设置: SetMonth(int month)
- 要求有一个成员函数实现月份的获取: GetMonth()
struct DataInfo{ int year; int month; int day; DataInfo(int year,int month,int day){ this->year = year; this->month = month; this->day = day; } DataInfo(){ year = 2015; month = 4; day = 2; } void SetDay(int day){ this->day = day; } int GetDay(){ return day; } void SetYear(int year){ this->year = year; } int GetYear(){ return year; } void SetMonth(int month){ this->month = month; } int GetMonth(){ return month; } };
2.设计一个结构TimeInfo
-
设计一个结构 TimeInfo,要求其满足下述要求:
- 该结构中包含表示时间的时、分、秒
- 设置该结构中时、分、秒的函数
- 获取该结构中时、分、秒的三个函数:GetHour(),GetMinute()和GetSecond()
struct TimeInfo{ int hour; int minute; int second; void SetHour(int hour){ this->hour = hour; } void SetMinute(int minute){ this->minute = minute; } void SetSecond(int second){ this->second = second; } int GetHour(){ return hour; } int GetMinute(){ return minute; } int GetSecond(){ return second; } };
3.练习继承
-
让TimeInfo继承DateInfo,分别使用DataInfo和TimeInfo的指针访问TimeInfo对象的成员
#include "stdafx.h" struct DataInfo{ int year; int month; int day; DataInfo(int year,int month,int day){ this->year = year; this->month = month; this->day = day; } DataInfo(){ year = 2015; month = 4; day = 2; } void SetDay(int day){ this->day = day; } int GetDay(){ return this->day; } void SetYear(int year){ this->year = year; } int GetYear(){ return year; } void SetMonth(int month){ this->month = month; } int GetMonth(){ return month; } }; struct TimeInfo:DataInfo{ int hour; int minute; int second; void SetHour(int hour){ this->hour = hour; } void SetMinute(int minute){ this->minute = minute; } void SetSecond(int second){ this->second = second; } int GetHour(){ return hour; } int GetMinute(){ return minute; } int GetSecond(){ return second; } }; void Test(){ //分别使用DataInfo和TimeInfo的指针访问TimeInfo对象的成员 TimeInfo t; t.SetHour(21); t.SetMinute(8); t.SetSecond(52); TimeInfo* tp = &t; printf("%04d.%02d.%02d - %02d:%02d:%02d\n",tp->year,tp->month,tp->day,tp->hour,tp->minute,tp->second); DataInfo* dp = &t; printf("%04d.%02d.%02d - %02d:%02d:%02d\n",dp->year,dp->month,dp->day,(dp + 1)->year,(dp + 1)->month,(dp + 1)->day); } int main(int argc, char* argv[]){ Test(); return 0; }
4.设计一个结构MyString
-
设计一个结构叫做MyString,要求该结构能够完成以下功能:
- 构造函数能够根据实际传入的参数分配实际存储空间
- 提供一个无参的构造函数,默认分配大小为1024个字节
- 析构函数释放该空间
- 编写成员函数SetString,可以将一个字符串赋值给该结构
- 编写成员函数PrintString,可以将该结构的内容打印到屏幕上
- 编写成员函数AppendString,用于向已有的数据后面添加数据
- 编写成员函数Size,用于得到当前数据的真实长度
#include "stdafx.h" #include <malloc.h> #include <string.h> /* 设计一个结构叫做MyString,要求该结构能够完成以下功能: - 构造函数能够根据实际传入的参数分配实际存储空间 - 提供一个无参的构造函数,默认分配大小为1024个字节 - 析构函数释放该空间 - 编写成员函数SetString,可以将一个字符串赋值给该结构 - 编写成员函数PrintString,可以将该结构的内容打印到屏幕上 - 编写成员函数AppendString,用于向已有的数据后面添加数据 - 编写成员函数Size,用于得到当前数据的真实长度 */ struct MyString{ char* str; MyString(){ str = (char*)malloc(1024); } ~MyString(){ free(str); str = NULL; } void allocate(int size){ str = (char*)malloc(size); } void SetString(char* str){ strcpy(this->str,str); } void PrintString(){ printf("%s\n",str); } void AppendString(char* str){ strcat(this->str,str); } int Size(){ return strlen(str); } }; void Test(){ MyString str; //分配空间 str.allocate(10); //给该对象赋值,并打印 char* string = "abcde"; str.SetString(string); str.PrintString(); //abcde //给该对象追加字符,并打印 str.AppendString("fgh"); str.PrintString(); //abcdefgh //打印现在对象的真实长度 printf("%d\n",str.Size()); //8 } int main(int argc, char* argv[]){ Test(); return 0; }
结果验证正确: