类–封装性(第一篇)
1.类VS对象
对象:
在现实生活中,任何事物都可以称为对象。
//每一个对象都具有动态属性和静态属性,一般将对象的数值属性称为静态属性,将对象可进行的动作称为动态属性。
类:
类 即 类型 之意,指编程者自己定义的一种新的数据类型。
类和对象的关系:
对象就是类的变量。
//结合生活实例理解:譬如在现实生活中,将 人 看成是所谓的“类”,那么 每个具体的人 就是所谓的 “对象”。
2.结构体VS类
结构体:描述对象的静态属性
类:把对象可进行的操作也加入到对象的描述中。
3. 补充学习 类 所需要的其他知识点:
//给结构体成员赋值(两种方法):
a.对成员一个一个赋值:
字符串类:
strcpy(a.name,“wang”);
strcpy((*pa).name, “wang”);
strcpy(pa->name, “wang”);
字符类:
a.sex = ‘F’;
(*p).sex = ‘F’;
p->sex = ‘F’;
数值类:
a.age = 20;
(*p).age = 20;
p->age = 20;
b.定义时,对结构体整个直接赋值(初始化):
person a = {“wang”, ‘F’, 20};
4. 类和对象的定义格式
a. 类的定义格式:
class <类名>
{
private:
<数据成员及成员函数>
protected:
<数据成员及成员函数>
public:
<数据成员及成员函数>
};
说明:
//private protected public 表示访问权限:私有的 保护的 公有的
//private 类内可直接访问 类外不可直接访问
//protected 类内可直接访问 类外不可直接访问
// public 类内可直接访问 类外可直接访问
//private protected public 出现次序不定;出现次数不定
//定义”类“时,private可省略
注意理解一下 “类内” 和 “类外” 的含义:
所谓"类内"是指在类的成员函数中;所谓"类外"是指在类的成员函数之外,即通过对象名或通过对象指针访问对象成员的成员。
b.对象的定义格式(定义"类"类型的变量)
"类"类型的变量 在面向对象编程部分,常称为对象,也称为类的实例
// "类"类型的变量 定义格式:
<类名> <对象列表>;
//例子:
Person a, b; //定义了Person类型的两个对象
Person *pa, *pb, *px, x[10]; //定义了Person类型的三个指针pa, pb, px以及Person类型的一个数组x[10]
pa = &a; pb = &b; px = x;
c.类和对象
与结构体类似,定义类的对象有三种方法:
—定义完成后定义对象
class Data
{
......
};
Data d1, d2;
—定义同时定义对象
class Data
{
......
} d1, d2;
—定义无类型对象
class
{
......
} d1, d2;
//缺点:只能定义一次该类对象,因为没有类型名,即没有数据类型标识符
//a.和b.和c. 知识点-实例:
#include <iostream>
using namespace std;
//定义Point类
class Point
{
private: //此处的 "private:"可省略
int x;
int y;
public:
void SetPoint(int a, int b)
{
x = a; //类内直接访问私有成员 x
y = b; //类内直接访问私有成员 y
}
void ShowPoint()
{
cout << Getx() << ',' << Gety() << endl; //类内直接访问公有成员函数 Getx()和 Gety() //并且其前无需函数的原型声明
}
int Getx()
{
return x;
}
int Gety()
{
return y;
}
};
int main()
{
Point *p, a;//定义Point类的指针p和变量(对象)a
a.SetPoint(1,1);
//通过对象名在类外直接访问类的公有成员函数
cout << a.Getx() << ',' << a.Gety() << endl; //A
a.ShowPoint(); //B
//通过"类"类型的指针直接访问类的公有成员函数
p = &a;
p->SetPoint(2,2);
cout << p->Getx() << ',' << p->Gety() << endl; //C
p->ShowPoint(); //D
return 0;
}
//测试结果
1,1 //对应A行
1,1 //对应B行
2,2 //对应C行
2,2 //对应D行
//说明:在类外,"a.x;" "p->x" 均为非法语句,在类外不可以通过对象名或"类"类型指针直接访问类的私有数据成员,只能通过公有成员函数接口间接访问类的私有数据成员。
一般将类的数据成员设计为私有的或者保护的(私有的更常见);为了能存取数据成员的值,需要设计公有成员函数(也叫类的方法)来访问类的数据成员。
d.类的成员函数的定义可以在类体内也可以在类体外
//例子:
//类内定义公有成员函数的方法
class Person //class 关键字;Person 类的类型名
{
private: //private 可省略 ;数据成员列表不用加{}
char Name[20];
char Sex;
int Age;
public:
void SetData(char na[], char s, int a) //成员函数1:修改类的私有数据成员的值(存)
{
strcpy(Name, na); //直接访问私有数据成员Name
Sex = s; //直接访问私有数据成员Sex
Age = a; //直接访问私有数据成员Age
}
void GetName(char *na) //成员函数2:获取私有成员Name的值(取)[用指针取回值]
{
na = Name;
}
char GetSex() //成员函数3:获取私有成员Sex的值(取)[用return取回值]
{
return Sex;
}
int GetAge() //成员函数4:获取私有数据成员Age的值(取)[用return取回值]
{
return Age;
}
};
//类外定义公有成员函数的方法
class Person //class 关键字;Person 类的类型名
{
private: //private 可省略 ;数据成员列表不用加{}
char Name[20];
char Sex;
int Age;
public:
void SetData(char na[], char s, int a);//成员函数原型声明
void GetName(char *na);
char GetSex();
int GetAge();
};
void Person::SetData(char na[], char s, int a) //成员函数1:修改类的私有数据成员的值(存)
{
strcpy(Name, na); //直接访问私有数据成员Name
Sex = s; //直接访问私有数据成员Sex
Age = a; //直接访问私有数据成员Age
}
void Person::GetName(char *na) //成员函数2:获取私有成员Name的值(取)[用指针取回值]
{
na = Name;
}
char Person::GetSex() //成员函数3:获取私有成员Sex的值(取)[用return取回值]
{
return Sex;
}
int Person::GetAge() //成员函数4:获取私有数据成员Age的值(取)[用return取回值]
{
return Age;
}
//总结:在类体外定义成员函数:类体内 成员函数的原型声明 + 类外 函数名前加类名加::
//在编程界,<类名>:: 叫做 "类名限定"(专业术语)。
e.类中数据成员类型的有关说明
—类中数据成员类型 可以是 : 基本数据类型(整型、实型、字符型等);构造数据类型(数组、指针、结构体、引用等);已定义的"类"的对象/指针/引用;自身类的指针/引用。
不可以 :自身类的对象。
—当定义一个类时使用了另一个类,而另一个类的定义在当前类的后面,则要类似函数原型声明对另一个类做引用性说明。
//例子:
class Date;
class Person
{
......
Date d1, d2;
Date *d;
......
};
class Date
{
......
};
f.关于对象的存储空间
一个对象占用的存储空间是对象自身数据成员的存储空间。
解释:系统为每个对象分配存储空间,用于存放该对象具体的数据成员值;所有对象的成员函数的代码是相同的,因此系统将成员函数的存储空间处理成该类的所有对象共享同一代码空间。
//例子:
clss Person
{
private:
char Name[20];
char Sex;
int Age;
public:
......
};
//Person 类的一个对象理论上占用的存储空间是25字节,但实际上在VS2013系统中sizeof(Person)的值是28,因为一般分配的存储空间的字节数是4的整数倍。
5.结构体VS类
a.结构体是类的特例
b.结构体:成员的默认访问权限是公有的
类:成员的默认访问权限是私有的
其他情况完全相同,如在结构体中也可以定义成员函数
c.由于结构体成员的缺省访问是公有的,所以在定义结构体变量时可以通过赋初值的方式对其初始化,对于类,如果其成员是公有的才能在定义对象时用上述的形式对其进行初始化。
问题:我们现在知道了 对象的私有数据成员在定义对象时不能通过赋初值的方式初始化,那么存不存在其他方法,使得对象的私有数据成员在定义对象时能被初始化?
答案是肯定的。
下面,我们开始介绍 类的两个特殊的成员函数:构造函数和析构函数。
6.构造函数
·功能:在定义类的对象时对对象的(私有)数据成员进行初始化。
·定义格式(有两种):
// 在类体内定义构造函数
ClassName(<形参列表>)
{..................}
//在类体外定义构造函数
ClassName::ClassName(<形参列表>)
{..................}
·特点:
a.可以根据需要定义一个或多个构造函数(重载)
b.函数名与类名相同,且不给出返回值
c. 一般将构造函数定义为公有成员函数
d. 不可以通过对象名调用构造函数
//注意和普通的成员函数区分开。
d1.Date(2009); //非法
e. 可以直接通过 构造函数名调用构造函数 创建对象
//也可以不自己显式调用构造函数,让系统在创建对象时自动调用构造函数。
//例子:
Date d1 = Date(2016);
Date d[4] = {Date(), Date(2016), Date(2016, 10), Date(2016, 10, 6)};
.实例:定义日期类,利用构造函数初始化日期对象的数据成员。
#include <iostream>
using namespace std;
class Date
{
private:
int Year, Month, Day;
public:
Date() //重载构造函数1
{
Year = 2016;
Month = 5;
Day = 1;
}
Date(int y) //重载构造函数2
{
Year = y;
Month = 5;
Day = 1;
}
Date(int y, int m) //重载构造函数3
{
Year = y;
Month = m;
Day = 1;
}
Date(int y, int m, int d) //重载构造函数4
{
Year = y;
Month = m;
Day = d;
}
void ShowDate() //公有成员函数
{
cout << Year << '.' << Month << '.' << Day << endl;
}
};
int main()
{
Date d1, d2(2016), d3(2016, 10), d4(2016, 10, 6);
//d1 系统自动调用重载构造函数1;d2 系统调用重载构造函数2;以此类推~~~
d1.ShowDate();
d2.ShowDate();
d3.ShowDate();
d4.ShowDate();
return 0;
}
//上述四个构造函数可以合并简化为一个
//class Date
//{
// private:
// int Year, Month, Day;
// public:
// Date(int y = 2016, int m = 5, int d = 1) //这种模式需要理解记忆着
// {
// Year = y;
// Month = m;
// Day = d;
// }
// void ShowDate()
// {
// cout << Year << '.' << Month << '.' << Day << endl;
// }
//};
//测试结果
2016.5.1
2016.5.1
2016.10.1
2016.10.6
7.析构函数
·功能:系统在撤销对象时,自动调用,做一些清理工作
特别注意:撤销对象和调用析构函数不是一回事。
·定义格式(有两种):
//在类体内定义析构函数
~ClassName()
{.......}
//在类体外定义析构函数
ClassName::~ClassName()
{........}
·特点:
a.函数名是类名,并且函数名前加"~",用来与构造函数区分
b.一般将析构函数定义为公有成员函数。
c.析构函数没有参数,且不给出返回值类型,因此一个类只能定义一个析构函数,即析构函数不允许重载。
d.析构函数一般由系统自动调用,但也可以由编程者安排通过对象显式调用
//例子:
a.~ClassName();
//显式调用析构函数
//此方法要慎用,一段空间释放两次,会出现运行错误。
·实例:定义学生类,利用构造函数初始化数据成员,利用析构函数做清理工作。
#include <iostream>
#include <cstring>
using namespace std;
class Student
{
private:
char Num[10]; //学号
char *Name; //姓名
int Score; //成绩
public:
Student(char *nump = NULL, char *namep = NULL, int scorep = 0) //构造函数
{
if(nump)
strcpy(Num, nump);
if(namep)
{
Name = new char[strlen(namep) + 1]; //动态申请姓名存储空间,使指针Name指向该空间
strcpy(Name, namep);
}
if(scorep)
Score = scorep;
cout << "Constructor Called!" << endl; //自己附加的调试语句
}
~Student() //析构函数 //在析构函数中,需要释放Name指向的空间
{
if(Name)
delete []Name;
cout << "Destructor Called!" << endl; //自己附加的调试语句
}
void Show()
{
cout << Num << ' ' << Name << ' ' << Score << endl;
}
};
int main()
{
char nump[10] = {"040120518"};
char namep[10] = {"George"};
Student a(nump, namep, 80); //编译警告:不赞成将字符串常量转化为char *型 //不要写成 Student a("040120518", "George", 80);
a.Show();
return 0;
}
//测试结果
Constructor Called! //系统自动调用了构造函数
040120518 George 80 //调用了一般成员函数Show()
Destructor Called! //系统自动调用了析构函数
//函数strcpy()的两个参数是指针类型的
//特别注意一个知识点:创建对象时,对姓名指针只分配了字符指针的存储空间,并没有分配它指向的用于存储字符串的空间。因此必须在构造函数中动态申请空间用于存储姓名字符串。
8.缺省构造函数(也称 默认构造函数)
·补充知识点:
缺省 【计算机术语】
外文名:default
释义:系统默认状态
缺省,可理解为 “省略的值为默认值” 故 省 读 shěng
·形式(有三种):
//例子
形式一:所有参数都带有缺省值(默认值)
Date(int y = 2000, int m = 1, int d = 1)
{
Year = y;
Month = m;
Day = d;
}
形式二:没有参数
Date()
{
Year = 2000;
Month = 1;
Day = 1;
}
形式三:没有参数,函数体为空
ClassName::ClassName()
{}
//例子
Date::Date()
{}
·特别需要注意的点:
a.形式三不同于形式一/形式二,形式三是系统自动生成的缺省构造函数形式。
b.在一个类的定义中,缺省构造函数(形式一/形式二)只能有一个。若在定义类时同时给出了两个,不同编译器会给出不同的处理方式,VC++6.0将报错;VS2013将给出警告信息,编译器处理成调用书写在前面的一个缺省构造函数。
c.在产生对象时,若不需要对数据成员进行初始化,可以不显式定义缺省构造函数。
d.若编程者已经定义了一个构造函数,无论它是什么类型的构造函数,则编译系统不再自动生成缺省构造函数。即系统自动生成缺省构造函数的前提是:编程者不定义任何构造函数。
9.缺省析构函数
·出现时机:在定义类时,如果编程者没有定义析构函数,则编译器自动生成一个缺省析构函数。
·形式:
ClassName::~ClassName()
{}
·注意点:
a.函数体为空,即不做任何工作。
b.在撤销对象时,若不需要做任何结束工作,可以不显示定义析构函数。
c.在类中有动态申请存储空间时,必须显示定义析构函数,以撤销动态存储空间。
构造函数和析构函数自己不定义的话是系统默认有的,是自动调用的,自定义的构造函数和析构函数只是附加了自己想要的东西。
10.拷贝构造函数
拷贝构造函数是一种特殊的构造函数。
·功能:用一个已知对象初始化一个新创建的同类对象。
·定义格式:
//在类体内定义
ClassName([const] ClassName &Obj)
{
......
}
//在类体外定义
ClassName::ClassName([const] ClassName &Obj)
{
......
}
·注意点:
a.const是关键字,若给出,表示形参Obj在拷贝构造函数的函数体中是对象常量(也称常对象)。
b.在类外定义时,在类内要进行函数的原型声明。
·拷贝构造函数的特点:
函数名与类名相同,不给出函数的返回值类型,函数参数有且只有一个,且必须是本类对象的引用,一般为常引用。
11.缺省拷贝构造函数
·出现时机:每个类一定有一个拷贝构造函数,如编程者在定义类时没有定义拷贝构造函数,则系统会自动生成缺省拷贝构造函数。
·形式:
//例子
Point::Point(const Point &p)
{
x = p.x;
y = p.y;
}
//讲一点题外话:我们之前提到,对象的私有数据成员在类内可以直接访问,在类外不能通过对象名或对象指针访问。在这里,我们应当注意到在拷贝构造函数内,通过对象名访问了对象的私有数据成员。
·作用:实现将参数对象p的数据成员值逐个赋值给新创建的对象的各个数据成员。
·特别注意点:
如果类的数据成员不需要动态申请空间,则编程者可以不自己定义拷贝构造函数,让系统自动生成缺省拷贝构造函数;但是如果类的数据成员需要动态申请空间,则编程者必须自己定义拷贝构造函数。
//原因实例讲解:由于编程者没有自己定义拷贝构造函数,程序运行出错。
#include <iostream>
#include <cstring>
using namespace std;
class Student
{
private:
int Age;
char *Name; //要动态申请空间
public:
Student(char *namep, int age)//构造函数
{
Age = age;
if(namep)
{
Name = new char[strlen(namep) + 1];
strcpy(Name, namep);
}
else
Name = NULL;
}
~Student()//析构函数
{
if(Name)
delete []Name;
}
void Show()//普通成员函数
{
cout << Name << ',' << Age << endl;
}
};
int main()
{
char namep[10] = {"George"};
Student a(namep, 20); //A
Student b = a; //B
b.Show(); //C
return 0;
}
此程序运行时,在输出了"George,20"后出错
为什么会出错呢?让我们来分析一下:
主函数从上到下看下来:A行调用构造函数,B行调用拷贝函数,由于定义类时没有自己定义拷贝函数,系统自动生成缺省拷贝构造函数,形式如下
Student::Student(const Student &p) //"浅"拷贝构造函数
{
Age = p.Age;
Name = p.Name;
}
新对象b实现了被已知对象a初始化,但是问题来了,由于 Name = p.Name;对象b和对象a的Name指针指向了同一段内存空间,在调用析构函数释放内存空间时,此段内存空间会被释放两次,出现了运行错误(RE:run error)。
补充:若拷贝函数的工作只是简单地把数据成员自身的值依次赋值给新创建对象的数据成员,则称为 "浅"拷贝,若没有动态申请空间,则使用系统自动创建的 "浅"拷贝构造函数就可以了 。
那么,为了解决程序运行错误,该如何做呢?
答案:定义 "深"拷贝构造函数,即自己定义拷贝构造函数,并在拷贝构造函数 动态申请一段新的内存空间,形式如下:
Student::Student(const Student &p)
{
Age = p.Age;
if(p.Name)
{
Name = new char[strlen(p.Name) + 1];
strcpy(Name, p.Name);
}
else
Name = NULL;
}
总结:什么时候一定要自己定义拷贝构造函数?
a.在产生新对象时,若只需拷贝同类型对象的部分数据成员
b.对象数据成员中有指针,指针指向动态申请的空间。
12.关于拷贝构造函数的调用时机
总的来说,只有当产生新对象,并且该对象由某个已知对象初始化的时候才会调用拷贝构造函数。
a.明确表示由一个对象初始化另一个对象
b.当对象作为函数参数时,系统处理成用实参对象初始化形参对象
c.当函数返回对象时,系统处理成返回值对象处理内存临时对象
//实例:
#include <iostream>
using namespace std;
class Point
{
private:
int x, y;
public:
Point(int a = 0, int b = 0) //缺省构造函数
{
x = a;
y = b;
cout << x << ',' << y << "Constructor Called." << endl;//调试语句
}
Point(Point &p); //拷贝构造函数原型声明
~Point() //析构函数
{
cout << x << ',' << y << "Destructor Called." << endl; //调试语句
}
void Show() //一般成员函数
{
cout << "Point:" << x << ',' << y << endl;
}
int Getx() //一般成员函数
{
return x;
}
int Gety() //一般成员函数
{
return y;
}
};
Point::Point(Point &p)
{
x = p.x;
y = p.y;
cout << x << ',' << y << "Copy-initialization Constructor Called." << endl; //调试语句
}
Point move(Point p, int xoffset, int yoffset) //Point 类 返回值;Point 类参数
{
int x = p.Getx() + xoffset;
int y = p.Gety() + yoffset;
Point t(x, y);
return t;
}
int main()
{
Point p1(6, 8), p2;
Point p3(p1); //或 Point p3 = p1;
Point s1(1, 2), s2(3, 4), s3(5, 6);
Point a[3] = {s1, s2, s3}; //调用三次拷贝构造函数
p2 = move(p1, 2, 4);
p2.Show();
p2 = p1;
p2.Show();
return 0;
}
//测试(理论/预期)结果
6,8Constructor Called. //p1调用缺省构造函数
0,0Constructor Called. //p2调用缺省构造函数
6,8Copy-initialization Constructor Called. //p3调用拷贝构造函数
1,2Constructor Called. //s1调用缺省构造函数
3,4Constructor Called. //s2调用缺省构造函数
5,6Constructor Called. //s3调用缺省构造函数
1,2Copy-initialization Constructor Called. //a[0]调用拷贝构造函数
3,4Copy-initialization Constructor Called. //a[1]调用拷贝构造函数
5,6Copy-initialization Constructor Called. //a[2]调用拷贝构造函数
6,8Copy-initialization Constructor Called. //Point p = p1;调用拷贝构造函数
8,12Constructor Called. //Point t(x, y);调用缺省构造函数
8,12Copy-initialization Constructor Called.//??怎么少了一个Point tmp = t; ????
8,12Destructor Called. //还少了撤销tmp
8,12Destructor Called. //撤销t
6,8Destructor Called. //撤销p
Point:8,12
Point:6,8
5,6Destructor Called. //撤销a[2]
3,4Destructor Called. //撤销a[1]
1,2Destructor Called. //撤销a[0]
5,6Destructor Called. //撤销s3
3,4Destructor Called. //撤销s2
1,2Destructor Called. //撤销s1
6,8Destructor Called. //撤销p3
6,8Destructor Called. //撤销p2
6,8Destructor Called. //撤销p1
//说明:析构函数撤销对象的顺序是创建对象的反序。
实际上:在不同编译器跑出来的结果是不一样的
//这里需要插入一个关于编译器的问题:
在VS2017的运行结果如下
6,8Constructor Called. //p1调用缺省构造函数
0,0Constructor Called. //p2调用缺省构造函数
6,8Copy-initialization Constructor Called. //p3调用拷贝构造函数
1,2Constructor Called. //s1调用缺省构造函数
3,4Constructor Called. //s2调用缺省构造函数
5,6Constructor Called. //s3调用缺省构造函数
1,2Copy-initialization Constructor Called. //a[0]调用拷贝构造函数
3,4Copy-initialization Constructor Called. //a[1]调用拷贝构造函数
5,6Copy-initialization Constructor Called. //a[2]调用拷贝构造函数
6,8Copy-initialization Constructor Called. //Point p = p1;调用拷贝构造函数
8,12Constructor Called. //Point t(x, y);调用缺省构造函数
8,12Copy-initialization Constructor Called.//有 Point tmp = t;
8,12Destructor Called. //撤销tmp
8,12Destructor Called. //撤销t
6,8Destructor Called. //撤销p
Point:8,12
Point:6,8
5,6Destructor Called. //撤销a[2]
3,4Destructor Called. //撤销a[1]
1,2Destructor Called. //撤销a[0]
5,6Destructor Called. //撤销s3
3,4Destructor Called. //撤销s2
1,2Destructor Called. //撤销s1
6,8Destructor Called. //撤销p3
6,8Destructor Called. //撤销p2
6,8Destructor Called. //撤销p1
在VS2017跑出来的结果和理论结果一致。
然鹅···
在Dev-C++跑出的结果是酱紫的:
6,8Constructor Called. //p1调用缺省构造函数
0,0Constructor Called. //p2调用缺省构造函数
6,8Copy-initialization Constructor Called. //p3调用拷贝构造函数
1,2Constructor Called. //s1调用缺省构造函数
3,4Constructor Called. //s2调用缺省构造函数
5,6Constructor Called. //s3调用缺省构造函数
1,2Copy-initialization Constructor Called. //a[0]调用拷贝构造函数
3,4Copy-initialization Constructor Called. //a[1]调用拷贝构造函数
5,6Copy-initialization Constructor Called. //a[2]调用拷贝构造函数
6,8Copy-initialization Constructor Called. //Point p = p1;调用拷贝构造函数
8,12Constructor Called. //Point t(x, y);调用缺省构造函数
//??怎么少了一个Point tmp = t; ????
//还少了撤销tmp
8,12Destructor Called. //撤销t
6,8Destructor Called. //撤销p
Point:8,12
Point:6,8
5,6Destructor Called. //撤销a[2]
3,4Destructor Called. //撤销a[1]
1,2Destructor Called. //撤销a[0]
5,6Destructor Called. //撤销s3
3,4Destructor Called. //撤销s2
1,2Destructor Called. //撤销s1
6,8Destructor Called. //撤销p3
6,8Destructor Called. //撤销p2
6,8Destructor Called. //撤销p1
我们发现:少了所谓 内存临时对象的创建和撤销。
这是为什么呢?
经博主查阅一些资料和询问他人,目前博主只能对此做出一点浅层的解释:Dev-C++的编译器可能开启了优化,才导致我们看不到内存临时对象的创建和撤消(至于怎么优化,博主现有能力不足,无法给出解释),但读者要注意,不能理解为有的编译器会在函数返回过程创建内存临时对象,而有的编译器则不会创建,因为函数返回创建内存临时对象是C++规定的,跟不同编译器无关。
[希望知道详尽原理的大佬们能在下方留言,大家一起共同学习!]
//总结:类中的数据成员出现指针时,要在构造函数中动态申请内存空间,在析构函数中释放内存空间,在拷贝构造函数中申请新空间。
13."类"类型和其他数据类型的转换
怎么实现 "类"类型和其他数据类型的转换 呢?
答案:利用 构造函数。
//实例引入:(假设定义了复数类Complex)
Complex c;
double x;
c = x; //A:将double型量赋值给Complex型量
x = c; //B:将Complex型量赋值给double型量
//对于A情况,按常识走,应将x作为实部,0做为虚部,创建一个Complex类的对象,赋值给c对象。
//对于B情况,按常识走,应将c对象的实部取出赋值给x。
//现在来分析A情况实际实现过程,B情况的分析留由后面的文章讲解。
//实例
#include <iostream>
using namespace std;
class Complex
{
private:
double Real, Image;
public:
Complex(double x = 0, double y = 0) //缺省构造函数
{
Real = x;
Image = y;
Show();
cout << "调用了构造函数" << endl;
}
~Complex() //析构函数
{
Show();
cout << "调用了析构函数" << endl;
}
void Show()
{
cout << '(' << Real << ',' << Image << ')';
}
};
int main()
{
Complex c1(3, 5), c2;
c1 = 8.0; //A
c2 = Complex(9.0, 9.0); //B
return 0;
}
//测试结果
(3,5)调用了构造函数 //c1 系统自动调用构造函数
(0,0)调用了构造函数 //c2 系统自动调用构造函数
(8,0)调用了构造函数 //系统自动调用构造函数 创建临时对象 //特别注意:A行右边虽然只有一个值8.0,但创建出来的对象是(8, 0) 而不是(8, 5)
(8,0)调用了析构函数 //系统自动调用析构函数 撤销临时对象 //临时对象赋值完就马上撤销了,不会等到最后才撤销
(9,9)调用了构造函数 //系统自动调用构造函数 创建临时对象
(9,9)调用了析构函数 //系统自动调用析构函数 撤销临时对象
(9,9)调用了析构函数 //系统自动调用析构函数 撤销对象 c2
(8,0)调用了析构函数 //系统自动调用析构函数 撤销对象 c1
重点掌握实现过程:
//Complex类类型和其他数据类型转化实现分析: 编译器将A行处理为c1=Complex(8, 0); 在等号右边调用构造函数创建临时对象,然后将该对象赋值给c1,赋值完成后撤销临时对象。
// B 行处理过程和A行一致。
14.成员函数的特性
a.内联函数和外联函数
·学会区分:类的成员函数什么时候是内联函数,什么时候是外联函数。
//在类体内定义的成员函数是内联函数;在类体外定义的成员函数是外联函数。
//当类体外定义的成员函数 的函数头前加 inline 关键字时,该成员函数变为内联函数。
//实例:
#include <iostream>
using namespace std;
class Complex
{
private:
double Real, Image;
public:
void SetRI(double r, double i) //内联函数
{
Real = r;
Image = i;
}
void GetRI(double &, double &); //引用取值 //函数原型声明
void Show(); //函数原型声明
};
inline void Complex::GetRI(double &r, double &i) //在类外定义的内联函数:一定要在函数头前加上 inline !!!并且不要忘记在函数名前加类名限定
{
r = Real;
i = Image;
}
void Complex::Show() //外联函数
{
cout << '(' << Real << ',' << Image << ')' << endl;
}
int main()
{
Complex c;
double r, i;
c.SetRI(5, 6);
c.GetRI(r, i);
cout << '(' << r << ',' << i << ')' << endl; //A行
c.Show(); //B行
return 0;
}
//测试结果
(5,6) //A行运行结果
(5,6) //B行运行结果
内联函数在类体外的定义也称为内联函数的实现。
·为什么成员函数会分为内联函数和外联函数呢?
//因为C++在处理内联函数和外联函数的调用过程有所不同。
//C++处理内联函数的调用过程:
先保护主调函数的执行现场,然后流程转入被调函数的函数体,执行完毕再转回到函数调用处,恢复现场,继续执行主调函数后面的语句。
//C++处理外联函数的调用过程:
编译时将函数调用替换成内联函数的代码,因此程序执行时没有发生实际的函数调用。
·内联函数VS外联函数
//根据C++对这两类函数的不同处理方式,不难看出 当使用内联函数时,优点:可以减少程序执行时的时间开销,提高了程序的执行效率;缺点:总代码空间加大。
//总结:一般将函数体较短,需频繁调用的函数定义成内联函数。这样函数的代码空间不会膨胀得很厉害,同时程序的执行效率也会提高。
·使用内联函数需要注意的一些点:
//内联函数一定要在调用之前定义
//内联函数不能递归调用
//内联函数的实现必须与其所属类的定义放在同一个文件中,否则编译时无法进行内联函数的替换。
b.成员函数的重载
15.对象成员
a.首先,理解一下 “对象成员” 的意思:
即 类的数据成员是另一个已定义类的对象。
//例子:
class Point
{
private:
int x, y;
public:
......
};
class Line
{
private:
int width, color;
Point p1, p2; //p1和p2叫做Line类的对象成员
public:
......
};
b.对象成员的初始化格式:
class ClassName
{
private:
ClassName_1 c1;
ClassName_2 c2;
ClassName_3 c3;
......
ClassName_n cn;
public:
ClassName(args):c1(arg_1), c2(arg_2), c3(arg_3), ... , cn(arg_n) //A 对 对象成员 进行初始化
{
......
}
......
};
//说明:ClassName_1 ClassName_2 ClassName_3 ... ClassName_n 是已定义的类;ClassName 是新定义的类
术语:冒号后用逗号隔开的对c1,c2,c3,… ,cn 初始化的列表称为构造函数的成员初始化列表。
//注意:args是形参,必须右类型说明;arg_1,arg_2,arg_3,… ,arg_n 是实参,不需要类型说明,他们 可以是来自args的变量,也可以是常量或表达式等。
//实例:
#include <iostream>
#include <cmath>
using namespace std;
class Point
{
private:
int x, y;
public:
Point(int a = 0, int b = 0)//缺省构造函数
{
x = a;
y = b;
cout << "构造Point:" <<x << ',' << y << endl;
}
int Getx()
{
return x;
}
int Gety()
{
return y;
}
~Point()
{
cout << "析构Point:" << x << ',' << y << endl;
}
};
class Line
{
private:
int width, color;
Point p1, p2;//这里不会调用构造函数初始化 p1 p2
public:
Line(int x1, int y1, int x2, int y2, int w, int c):p1(x1, y1), p2(x2, y2) //这里才调用Point的构造函数初始化 p1 p2,即对象成员p1,p2的初始化工作是在Line类的构造函数中完成的 //注意对象成员的初始化方法:冒号 逗号 无分号
{
width = w;
color = c;
cout << "构造Line:" << width << ',' << color << endl;
}
double LineLen()
{
double len;
int x1, y1, x2, y2;
x1 = p1.Getx();//这里调用对象p1 p2的成员函数跟在main()函数中调用对象的成员函数一样,用对象名直接访问
y1 = p1.Gety();
x2 = p2.Getx();
y2 = p2.Gety();
len = sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
return len;
}
~Line()
{
cout << "析构Line:" << width << ',' << color << endl;
}
};
int main()
{
Line Li(0, 0, 1, 1, 3, 6); //调用Line的构造函数
cout << "长度=" << Li.LineLen() << endl;
return 0;
}
//测试结果:
构造Point:0,0
构造Point:1,1
构造Line:3,6
长度=1.41421
析构Line:3,6
析构Point:1,1
析构Point:0,0
·注意调用构造函数的顺序:
//先调用对象成员的构造函数,再调用类自身的构造函数。
//进一步的:
//调用对象成员构造函数的顺序与书写在成员初始化列表中的顺序无关,而与对象成员的定义顺序有关,先定义的先调用。
16.this指针
a.this指针介绍
this是一个隐含于成员函数的特殊指针。该指针指向调用成员函数的当前对象。当对象调用成员函数时,系统自动将对象自身的指针(即对象的地址)传递给成员函数,在成员函数中可以直接使用该指针,指针名为this。
//实例:
#include <iostream>
using namespace std;
class Sample
{
private:
int x, y;
public:
Sample(int a = 0, int b = 0)
{
this->x = a;
this->y = b;
}
void Print()
{
cout << this << '\t';
cout << this->x << '\t';
cout << this->y << '\n';
}
};
int main()
{
Sample c1(1, 4), c2(3, 7);
cout << &c1 << endl;
c1.Print();
cout << &c2 << endl;
c2.Print();
return 0;
}
//测试结果
0x70fe10 //A
0x70fe10 1 4 //B
0x70fe00 //C
0x70fe00 3 7 //D
//说明:A B 行的地址值一样;C D行的地址值一样 :证明了this指针的存在。B D行不一样证明了this指针指向调用成员函数的当前对象。
//地址的具体值可以不一样的
b.this指针的用处:
区分成员函数参数与类的数据成员同名的情况。
//拓展:在类的数据成员之前加类名限定:也可区分成员函数参数和类的数据成员同名的情况。