内容
- 类和对象的定义
- 构造函数和析构函数
- 内联函数和外联函数
- 成员函数的重载
- this指针
累积了很多周的网课笔记,脑壳疼,po在知乎上方便复习啦。
一 类和对象的定义
1 类和对象的定义形式
1)
class <类名>
{ [ [private:] //私有成员,缺省存取权限
<数据成员及成员函数 > ]
[ public: //公有成员
<数据成员及成员函数 > ]
[ protected: //保护成员
<数据成员及成员函数 > ]
};
注:
私有成员:只允许类内成员函数存取它
公有成员:允许类内和类外函数存取它
保护成员:允许类内和其派生类函数存取它
例:
定义描述一个人的类 Person,源程序文件名为Person.h
#include <iostream>
#include <string>
using namespace std;
class Person
{ private: // 此处,private可缺省
char Name[20]; //姓名
char Sex; //性别,三个数据成员
int Age; //年龄
public: // 以下定义了四个成员函数
void SetData(char n[ ], char s, int a)
{
strcpy(Name, n); //直接访问Name
Sex=s; //直接访问Sex 访问特性
Age=a; //直接访问Age
}
void GetName( char *n ) // 函数
{
strcpy(n, Name);
}
char GetSex( ) // 函数
{
return Sex;
}
int GetAge( ) // 函数
{
return Age;
}
}; // 注意:类定义结束处的分号不能少
2)
类是一种类型,该类型的变量称为对象(实例)
对象的定义:
<类名> <对象列表>;
例:
Person a, b ; // 定义a、b两个对象
Person *pa, *pb, x[10]; // pa和pb是两个Person类型指针, x是对象数组
2 对象成员的访问
3 成员函数的定义
1)在类体内定义成员函数
见上
2)在类体外定义成员函数
#include <iostream> //程序文件名为Person.h
#include <string>
using namespace std;
class Person
{ char Name[20]; //姓名 //注意缺省 private
char Sex; //性别
int Age; //年龄
public:
void SetData(char [ ], char , int );
void GetName(char *);
char GetSex( );
int GetAge( );
};
void Person::SetData(char n[ ], char s, int a)
{
strcpy(Name, n);
Sex=s;
Age=a;
}
void Person::GetName( char *n )
{
strcpy(n, Name);
}
char Person::GetSex( )
{
return Sex;
}
int Person::GetAge( )
{
return Age;
}
3)主函数
#include "Person.h" //包含1)或2)中的头文件
using namespace std;
void main( )
{
Person a, *pa;
char name[20];
//以下通过对象访问成员
a.SetData("Cheng", 'F', 20);
a.GetName(name);
cout << "Name: " << name <<endl;
cout << " Sex: " << a.GetSex( ) << endl;
cout << " Age: " << a.GetAge( ) << endl;
//以下通过指针访问成员
pa=&a;
pa->SetData("Zhang", 'M', 18);
pa->GetName(name);
cout << "Name: " << name <<endl;
cout << " Sex: " << pa->GetSex( ) << endl;
cout << " Age: " << pa->GetAge( ) << endl;
}
4 对象的存储空间
对象存储空间的分配和撤销是系统根据对象的作用域自动完成的,即进入对象作用域时,系统自动为对象分配空间;退出对象作用域时,系统自动撤销对象空间。
系统为每个对象分配储存空间,用于存放该对象具体的数据成员值。所有对象的成员函数的代码是相同的,因此系统将成员函数的存储空间处理成该类的所有对象共享同一代码空间。因此一个对象的存储空间是对象自身数据成员的存储空间。
例:
Person a, b, *pa=&a, *pb=&b;
a.SetData("Cheng", 'F', 20);
b.SetData("Zhang", 'M', 18);
对象数据成员的存储空间
5 说明
1)类中数据成员的类型可以是任意的,但自身类的对象不可以作为成员。
class Person
{ char Name[20];
char Sex;
int Age;
Obj obj1, obj2; // 可以
Person a, b; // 不可以
Person *pa; // 可以
......
};
2) 若类的使用在前,定义在后,则需引用性说明。
class B; // B类说明
class A // A类的定义
{
private:
B *pb; // B类对象的指针pb是A类的数据成员
public:
......
};
class B // B类的定义
{
......
};
3) 定义对象的三种方法 :
class A // 1.A类的定义, 类的定义完成后,定义对象
{
int x;
float y;
public:
......
};
A a1, a2, a3; //定义A类对象
class A // 2.A类的定义,在定义类的同时定义对象
{
int x;
float y;
public:
......
} a1, a2, a3; //定义A类对象
class //3.无类名,定义无类名的对象(不推荐,只能定义一次该类对象)
{
int x;
float y;
public:
......
} a1, a2, a3; //定义该类对象
4)结构体与类的区别:
结构体成员缺省的存取权限是公有的,
而类中成员缺省的存取权限是私有的。
结构体是类的特例。
二 初始化对象、撤消对象
1 “初始化是指:定义对象的同时为其赋初值”
1)结构体初始化
struct SPerson //定义结构体类型
{
char name[20]; //姓名
char sex; //性别
int age; //年龄
};
SPerson p1={ “Cheng”, ‘F’, 20 }; //初始化 √
2)类初始化
class Person //定义“类”类型
{
public:
char name[20]; //姓名
char sex; //性别
int age; //年龄
};
Person p={ “Cheng”, ‘F’, 20 }; //初始化 √
可以为“类”定义构造函数用于初始化对象的数据成员,同时还可以定义析构函数用于撤销该对象时做清理工作。
2 构造函数和析构函数
1) 构造函数
在类体内定义构造函数的一般格式是:
ClassName(<形参列表>) // ClassName是类名,
{ ...... } //在此处作为构造函数名
在类体外定义构造函数的一般格式是:
ClassName :: ClassName(<形参列表>)
{ ...... }
特点:
(1) 构造函数是成员函数,函数体可写在类体内,也可写在类体外。
(2) 构造函数是一种特殊的函数,其函数名与类名相同,且不给出返回值类型。
(3) 构造函数可以重载,即可以定义多个参数个数以及参数类型不同的构造函数。
(4) 一般将构造函数定义为公有成员函数。
(5) 在创建对象时,系统自动调用构造函数。
(6) 不可以通过对象名调用构造函数。例如:d1.Date(2009) 是非法的。
(7) 可以直接通过构造函数名调用构造函数创建对象。例如:d1 = Date(2009); 等号右边创建了一个对象,并将它赋值给 d1。
2) 析构函数
在类体内定义析构函数的一般格式是:
~ClassName( ) // ClassName是类名
{ ...... } // 析构函数名为~ClassName
在类体外定义析构函数的一般格式是:
ClassName :: ~ClassName( )
{ ...... }
特点:
(1) 析构函数是成员函数,可在类体内定义,也可在类体外定义。
(2) 一般地,将析构函数定义成公有成员函数。
(3) 析构函数也是特殊函数,该函数的名字是类名前加 ~,用来与构造函数区分,该函数不指定返回值类型,没有参数。
(4) 一个类只能定义一个析构函数,即析构函数不允许重载。
(5)析构函数可以被显式调用,也可以由系统自动调用。 在下面两种情况下,析构函数会被自动调用:
①当对象是系统自动创建的,则在对象的作用域结束时,系统自动调用析构函数。
②当一个对象是使用new运算符被动态创建的,在使用delete运算符释放它时,delete将会自动调用析构函数。
3) 调用构造函数和析构函数的时机(对象的生存周期)
(1)全局对象(总是静态的) —— 程序main( )开始执行时,调用构造函数,程序main( )结束时,调用析构函数。
(2)局部动态对象 —— 当程序执行进入作用域,在定义对象处系统自动创建对象,调用构造函数;退出作用域时,系统在撤消对象前调用析构函数。
(3)局部静态对象 —— 当程序执行首次到达定义对象处,系统自动创建对象,调用构造函数;程序结束时(即main( )函数执行结束时),系统在撤消对象前调用析构函数。
(4)动态申请的对象 —— 使用 new 产生对象时,系统自动调用构造函数;使用 delete 撤消对象时,系统自动调用析构函数。
例:
#include <iostream>
using namespace std;
class Date
{
int Year, Month, Day;
public:
Date(int y = 2000, int m = 1, int d = 1)//A,所有参数都有默认值
{
Year = y; Month = m; Day = d;
cout << "Constructor: ";
ShowDate();
}
void ShowDate()
{
cout << Year << '.' << Month << '.' << Day << endl;
}
~Date()
{
cout << "Destructor: ";
ShowDate();
}
};
Date d4(2016, 4, 4); //全局对象(静态的)
void fun()
{
cout << "进入 fun()函数!\n";
static Date d2(2016, 2, 2); //局部静态对象
Date d3(2016, 3, 3); //局部动态对象
cout << "退出 fun()函数!\n";
}
int main()
{
cout << "进入 main()函数!\n";
Date d1(2016, 1, 1); //局部动态对象
fun();
fun();
cout << "退出 main()函数!\n";
return 0;
}
运行结果:
Constructor: 2016.4.4 //调用构造函数,产生d4对象
进入 main( )函数!
Constructor: 2016.1.1 // 调用构造函数,产生d1对象
进入 fun( )函数!
// 第1次进入fun( )函数,产生下述 d2, d3对象
Constructor: 2016.2.2
Constructor: 2016.3.3
退出 fun( )函数!
// 退出fun( )函数,撤消d3对象,不撤消d2对象
Destructor: 2016.3.3
进入 fun( )函数!
// 第2次进入fun( )函数,再次产生d3对象
Constructor: 2016.3.3
退出 fun( )函数!
Destructor: 2016.3.3 // 退出fun( )函数,撤消d3对象
退出 main( )函数!
// 退出main ( )函数,撤消d1, d2, d4对象
Destructor: 2016.1.1
Destructor: 2016.2.2
Destructor: 2016.4.4
3 缺省构造函数和缺省析构函数
1) 缺省构造函数
缺省构造函数也称为默认构造函数,通常是指在定义对象时,若不给初值(即构造函数的实参),则系统使用默认值初始化数据成员。
定义类时,若没有定义构造函数,则编译系统自动生成一个不带参数的缺省构造函数,格式为:ClassName:: ClassName( ) { }
例:
#include <iostream>
using namespace std;
class Date
{ int Year, Month, Day;
public:
void ShowDate( )
{ cout <<Year<<'.'<<Month<<'.'<<Day<<endl; }
};
void main( )
{
Date d;
d.ShowDate( );
}
运行结果:
-858993460.-858993460.-858993460
说明:
系统自动产生的缺省构造函数是:
Date::Date( )
{ } //不做任何操作
缺省构造函数有两种形式: 没有参数的构造函数或各参数均有缺省值的构造函数,缺省构造函数只能有一个。
Date:: Date( ) //没有参数
{
Year=2003; Month=1; Day=1;
}
或
Date:: Date(int y=2003, int m=1, int d=1) //均有缺省值
{
Year=y; Month=m; Day=d;
}
注:
①在产生对象时,若不需要对数据成员进行初始化,可以不显式地定义缺省构造函数。
②在一个类的定义中,缺省构造函数只能有一个。
③若已经定义了构造函数(不论它是否为缺省构造函数),则编译系统不再自动生成缺省构造函数。
2) 缺省析构函数
若在类中没有显式定义析构函数,则编译器自动地产生一个缺省的析构函数,格式为:ClassName :: ~ ClassName ( ) { };
注:
① 在撤消对象时,若不需要做任何结束工作,可以不定义析构函数。
② 当类中有动态申请的数据空间时,需要显式地定义析构函数,撤销动态数据空间。
4 拷贝构造函数和缺省拷贝构造函数
1)
拷贝构造函数:用一个已知的对象来初始化一个被创建的同类对象。
格式:
ClassName :: ClassName( [const] ClassName & Obj )
{ <函数体> } //注意参数是本类对象的引用
形参Obj是实参对象的引用,即它是实参对象的别名。const是关键字,如果给出,表示形参Obj在拷贝构造函数的函数体中是对象常量,不能被修改,以保护实参对象。
如果用户不定义拷贝构造函数,则系统自动产生一个缺省的拷贝构造函数。
当类中有动态申请的数据空间时,必须定义拷贝构造函数,以便实现成员数据的复制。同时应显式定义相应的析构函数,撤消动态分配的空间。
注:“深”拷贝与“浅”拷贝
若拷贝构造函数的工作只是简单地把数据成员的自身的值依次赋值给新创建对象的数据成员,则称为“浅”拷贝。“深”拷贝指除了将对象自身成员进行拷贝外,还需要完成动态空间的拷贝。
例:
#include <iostream>
#include <cstring>
using namespace std;
class Student
{
char *Name; //姓名,注意:用指针实现
int Age; //年龄
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()
{
Student a("George", 20);
Student b=a; // A
return 0;
}
此程序运行时出错,原因是: 没有定义类的拷贝构造函数。系统自动产生的拷贝构造函数如下:
Student::Student(const Student &s)
{
Name = s.Name; // 注意:地址值直接赋值
Age = s.Age;
}
缺省拷贝构造函数只负责对象自身的数据成员即Name和Age的拷贝工作(D、E行),即在程序的C行调用拷贝构造函数创建b对象后,b对象的Name与a对象的Name指向了相同的串空间。
假定”George”字符串的起始位置为1000
主函数结束后,系统首先撤销b对象,调用析构函数将b对象的成员Name指针指向串空间撤销;然后撤销a对象时就会出错。
正确的做法是,定义如下拷贝构造函数:
Student::Student(const Student &s)
{
Age=s.Age;
if(s.Name)
{
Name = new char[strlen(s.Name)+1]; //C
strcpy(Name, s.Name);
}
else Name=NULL;
}
2) 拷贝构造函数的调用时机
(1)明确表示由一个对象初始化另一个对象时(A、B意义相同),
例:
Point p3(p1); // A 用已知对象p1初始化正在创建的新对象p3,调用拷贝构造函数
Point p4=p2; // B用已知对象p2初始化正在创建的新对象p4,调用拷贝构造函数
(2)当用对象作为函数参数时,系统处理成用实参对象初始化形参对象。
例:
Point move(Point p, int xoffset, int yoffset)
{
int x = p.Getx( )+xoffset, y = p.Gety( )+yoffset;
Point t(x, y);
return t;
}
注:
Point p=p1 //参数传递时
(3)当函数返回对象时,用返回值对象初始化内存临时对象。
注:
Point 内存临时对象=t。//返回对象时
源代码:
头文件(point.h):
class Point
{
int x, y;
public:
Point(int a=0, int b=0) //缺省构造函数
{
x=a; y=b;
cout<<x<<','<<y<<" Constructor"<<endl;
}
Point(Point &p); //拷贝构造函数原型说明
~Point( ) //析构函数
{
cout<<x<<','<<y<<" Destructor Called.\n" ;
}
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.\n";
}
主文件:
#include <iostream>
using namespace std;
#include "point.h" //普通函数,不是类的成员函数
Point move(Point p, int xoffset, int yoffset)
{
int x = p.Getx( )+xoffset, y = p.Gety( )+yoffset;
Point t(x, y);
return t;
}
int main( )
{
Point p1(6, 8), p2;
p2=move(p1, 2, 4);
p2=p1;
return 0;
}
执行顺序:
Point p1(6, 8), p2; //调用了p1,p2的构造函数(局部动态变量)
p2=move(p1, 2, 4);
//函数move()的第一个形参以及函数的返回值均为Point类对象,第一个参数传递是Point p=p1,p是move()函数内部新创建的局部动态对象,此时调用拷贝构造函数
//当move()函数执行return t时,系统自动创建一个内存临时对象,用对象t初始化该内存临时对象,Point 内存临时对象=t,此时调用拷贝构造函数
// 在move()函数作用域调用构造函数,离开作用域后调用析构函数
p2=p1; //赋值运算,不调用拷贝构造函数,创建、撤销临时对象,调用构造、析构函数(DEV C++不显示,VC++显示)
离开main作用域后,分别调用p2,p1析构函数
运行结果:
3)利用构造函数进行类型转换
例:
#include <iostream>
using namespace std;
class Complex
{
double Real, Image;
public:
Complex(double x=0, double y=0)
{
Real=x; Image=y;
Show( );
cout << "调用了构造函数\n";
}
~Complex( )
{
Show( );
cout << "调用了析构函数\n";
}
void Show( )
{
cout<<'('<<Real<<','<<Image<<')';
}
};
int main( )
{
Complex c1(3, 5), c2; //A
c1 = 8.0; //B 等价于c1 = Complex(8.0);
c2 = Complex(9.0, 9.0); //C
return 0;
}
运行结果:
(3, 5)调用了构造函数 //在A行创建c1对象时,调用构造函数
(0, 0)调用了构造函数 //在A行创建c2对象时,调用构造函数
(8, 0)调用了构造函数
(8, 0)调用了析构函数 //B行,创建、撤销临时对象,调用构造、析构函数
(9, 9)调用了构造函数
(9, 9)调用了析构函数 //C行,创建、撤销临时对象,调用构造、析构函数
(9, 9)调用了析构函数 //在程序结束,撤消c2对象时,调用析构函数
(8, 0)调用了析构函数 //在程序结束,撤消c1对象时,调用析构函数
三 成员函数的特性
1 内联函数和外联函数
在类体内定义的成员函数是内联函数,在类体外定义的函数是外联函数。在类体外,也可以定义内联函数,方法是在函数定义的首部增加incline关键词。
例:
class Complex
{ double Real, Image;
public:
Complex(double x=0, double y=0); //在类体内,给出成员函数说明
void Show( );
};
inline Complex::Complex(double x, double y) //在类体外定义内联函数
{
Real=x; Image=y;
}
inline void Complex::Show( ) //在类体外定义内联函数
{
cout<<'('<<Real<<','<<Image<<')';
}
注:
1)内联函数的特点:
编译时在调用函数处用内联函数的代码来替换函数调用语句,这样在程序执行时,不需要函数调用,减少时间开销。代价是程序的总代码空间增大。
2)一定要在调用前定义,一般不超过5个分号句,且不能使用复杂语句(条件循环与递归等)。
3)内联函数的实现必须与所属类的定义放在同一个文件夹,否则编译时无法进行内联函数的替换。
2 成员函数的重载
构造函数可以重载,一般的成员函数也可以重载。
四 构造函数和对象成员
定义一个类时,可以把一个已定义的类的对象作为该类的成员。产生新定义类的对象时,须对它的对象成员进行初始化,且只能通过其对象成员的构造函数来实现。
例:
#include <iostream>
#include <cmath>
using namespace std;
class Point //定义"点"类
{
int x, y;
public:
Point(int a=0, int b=0)
{
x=a; y=b;
cout<<x<<','<<y<<" 构造 Point\n";
}
int Getx( )
{ return x; }
int Gety( )
{ return y; }
~Point( )
{ cout<<x<<','<<y<<" 析构 Point\n"; }
};
class Line //定义"线"类,两点决定一条线
{
int width, color; //指定"线"的宽度、颜色
Point p1, p2; //指定"线"的两个端点
public:
Line(int x1, int y1, int x2, int y2, int w, int c) : p1(x1,y1), p2(x2,y2) // A
{
width=w; color=c;
cout<<width<<','<<color<<"构造 Line\n";
}
double LineLen( )
{
double len;
int x1, y1, x2, y2;
x1=p1.Getx( ); y1=p1.Gety( );
x2=p2.Getx( ); y2=p2.Gety( );
len=sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2));
return(len);
}
~Line( )
{
cout<<width<<','<<color<<"析构 Line\n";
}
};
int main( )
{
Line Li(0, 0, 1, 1, 3, 6);
cout<<"长度="<<Li.LineLen( )<<endl;
return 0;
}
对象、内嵌对象执行顺序:
调用对象成员构造函数的顺序与书写在成员初始化列表中的顺序无关,而与对象成员的定义顺序有关,先定义的先调用。
如将例10.16中的A行改写为: Line(int x1, int y1, int x2, int y2, int w, int c): p2(x2, y2), p1(x1, y1) 仍然是先调用p1(x1, y1),再调用p2(x2, y2),因为p1的定义在前,p2的定义在后。
构造函数执行顺序:
先调用对象成员的构造函数,再调用类自身的构造函数,析构函数的调用顺序与构造函数的调用顺序相反。
所以,先调用p1的构造函数——P2的构造函数——Li的构造函数——Li的析构函数——P2的析构函数——P1的析构函数
运行结果:
五 this 指针
this是一个隐含于每一个类的成员函数中的特殊指针,它指向调用该函数的对象。当对象调用成员函数时,自动将对象自身的地址(指针),传递给成员函数,在成员函数中可直接使用该指针,指针名为this。
例:
#include <iostream>
using namespace std;
class Sample
{
int x, y;
public:
Sample( int a=0, int b=0)
{ x=a; y=b; }
void Print( )
{ cout << x <<'\n';
cout << y <<'\n';
}
};
void main( )
{ Sample c2(3, 5);
c2.Print( );
}
成员函数中隐含着一个指针this,它指向调用成员函数的对象,如下述程序与上述程序等价:
#include <iostream>
using namespace std;
class Sample
{ int x, y;
public:
Sample( int a=0, int b=0)
{ this->x = a; //在此例中this = &c2。
this->y = b;
}
void Print( )
{ cout << (this->x) <<'\n'; //在此例中this = &c2。
cout << (this->y) <<'\n';
}
};
void main( )
{ Sample c2(3, 5);
c2.Print( );
}