*** 目录 ***
-
封装 类 (构造函数,析构函数,常量,const ,静态数据成员,静态成员函数,友元) (5.9)
-
运算符的重载
*** --------***
暂时只有
3封装 类 (构造函数,析构函数,常量,const ,静态数据成员,静态成员函数,友元)
4 继承
一. 构造函数
对构造函数,说明以下几点:
1.构造函数的函数名必须与类名相同。构造函数的主要作用是完成初始化对象的数据成员以及其它的初始化工作。
2.在定义构造函数时,不能指定函数返回值的类型,也不能指定为void类型。
3.一个类可以定义若干个构造函数。当定义多个构造函数时,必须满足函数重载的原则。
4.构造函数可以指定参数的缺省值。
5.若定义的类要说明该类的对象时,构造函数必须是公有的成员函数。如果定义的类仅用于派生其它类时,则可将构造函数定义为保护的成员函数。
6.由于构造函数属于类的成员函数,它对私有数据成员、保护的数据成员和公有的数据成员均能进行初始化。
7、在类中,若定义了没有参数的构造函数,或各参数均有缺省值的构造函数也称为缺省的构造函数,缺省的构造函数只能有一个。
class A{
float x,y;
public:
A(float a, float b=10) { x=a; y=b; }
A() { x=0; y=0;}
void Print(void)
{
cout<<x<<'\t'<<y<<endl;
}
};
void main( )
{ A a1, a2(20.0), a3(3.0, 7.0);
a1.Print();
a2.Print();
a3.Print();
}
对局部对象,静态对象,全局对象的初始化,对于局部对象,每次定义对象时,都要调用构造函数。
对于静态对象,是在首次定义对象时,调用构造函数的,且由于对象一直存在,只调用一次构造函数。
对于全局对象,是在main函数执行之前调用构造函数的。
#include <iostream>
using namespace std;
class A
{
private :
int x;
int y;
public:
A(int a) { x = a; cout << "1" << endl; };
A(float a, float b = 10)
{
x = a;
y = b;
cout << "2" << endl;
}
};
A a1(3);
void f(void) { A b(2, 3); static A a3(4, 3); }
//static 只调用构造函数一次
void main()
{
A a2(4, 5);
f();
f();
}
结果是 1
a2 2
b 2
a3 2
b 2
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-spckjpwb-1620527632621)(C:\Users\hjkl\AppData\Roaming\Typora\typora-user-images\image-20210509083900656.png)]
显式定义就会把默认构造函数覆盖,就无法只定义一个对象
缺省构造函数
class A{
float x,y;
public:
A(float a=10,float b=20){ x=a; y=b; }
A(){ }
void Print(void){ cout<<x<<'\t'<<y<<endl; }
};
void main(void)
{ A a1
A a2(3.0,30.0);
}
两个函数都是缺省的构造函数;
a1 的调用不唯一
类指针
A *pa1,*pa2;
pa1=new A(3.0, 5.0);//用new动态开辟对象空间,初始化
pa2=new A;//用new动态开辟空间,调用构造函数初始化
pa1->Print();
pa2->Print();
指针调用的时候,要用 ->
指向对象成员的指针
1.指向对象数据成员的指针
int *p;
p=&t1.hour;
cout<<*p<<endl;
2.指向对象成员函数的指针
类型名(*指针变量名)(参数表列)
void (*p)()
p=fun; //p 可以指向一个函数fun
(*p)(); //调用fun函数
如果要调用类的函数
要写成 void (Time::*p2)();
p2=&Time::get_time;
创建(定义)
数据类型名(类名::*指针变量名)();
调用一个公有函数
p2=&Time::get_time;
指针变量名=&类名::成员函数名;
构造函数的重载
testClass();//不带参数的默认构造函数---------
testClass(int a, char b);//构造函数
testClass(int a = 0, char b = 'c');//参数都有默认值的默认构造函数--
class Box {
public :
Box(int h=10,int w=10,int len=10); //在声明构造函数时指定默认参数--默认构造函数
int volume( );
private :
int height;
int width;
int length;
};
Box::Box(int h,int w,int len)
{
height=h;
width=w;
length=len;
}
Box box1; //没有给实参
cout<<"The volume of box1 is "<<box1.volume( )<<endl;
Box box2(15); //只给定一个实参
cout<<"The volume of box2 is "<<box2.volume( )<<endl;
Box box3(15,30); //只给定2个实参
cout<<"The volume of box3 is "<<box3.volume( )<<endl;
Box box4(15,30,20); //给定3个实参
cout<<"The volume of box4 is "<<box4.volume( )<<endl;
二. 析构函数
1、析构函数是成员函数,函数体可写在类体内,也可写在类体外。
2.析构函数是一个特殊的成员函数,函数名必须与类名相同,并在其前面加上字符“~”,以便和构造函数名相区别。
3、析构函数不能带有任何参数,不能有返回值,不指定函数类型。
4、一个类中,只能定义一个析构函数,析构函数不允许重载。
5、析构函数是在撤消对象时由系统自动调用的。
class Str
{
char *Sp;
int Length;
public:
Str(char *string)
{
if(string)
{
Length=strlen(string);
Sp=new char[Length+1]; strlen 可以计算的长度是除去\n 的
strcpy(Sp,string);
}
else
Sp=0;
}
void Show(void){ cout<<Sp<<endl; }
~Str() {
if(Sp)
delete []Sp; }
};
void main(void)
{ Str s1("Study C++");
s1.Show();
}
delete 删除的是指针指向的那一块空间
class A
{
private :
int a;
static int b;
const int c;
public:
void h(int b) { this->b = b; }
A(int s, int m) : c(s) {
a = m;
}
};
int A::b = 0;
// 常量成员变量的初始化(Integral type) 用初始化列表,不能直接赋值
不同存储类型的对象调用构造函数及析构函数
1、对于全局定义的对象(在函数外定义的对象),在程序开始执行时,调用构造函数;到程序结束时,调用析构函数。
2、对于局部定义的对象(在函数内定义的对象),当程序执行到定义对象的地方时,调用构造函数;在退出对象的作用域时,调用析构函数。
3、用static定义的局部对象,在首次到达对象的定义时调用构造函数;到程序结束时,调用析构函数
4、对于用new运算符动态生成的对象,在产生对象时调用构造函数,只有使用delete运算符来释放对象时,才调用析构函数。若不使用delete来撤消动态生成的对象,程序结束时,对象仍存在,并占用相应的存储空间,即系统不能自动地调用析构函数来撤消动态生成的对象。
pa1=new A[3];
.....
delete [ ]pa1;
对象的引用
- 对象名.数据成员名 或
对象指针->数据成员名
arr1.storage 或 rp->num - 对象名.成员函数名(实际参数表) 或对象指针->成员函数名(实际参数表)
arr1.insert() 或 rp->add() - 外部函数不能引用对象的私有成员
this指针
-
当定义一个对象时,系统会为对象分配空间,用于存储对象的数据成员。而类中的成员函数对该类的所有对象只有一份拷贝。那么对于类的成员函数,它如何知道要对哪个对象进行操作呢?
-
每个成员函数都有一个隐藏的指向本类型的指针形参this,它指向当前调用成员函数的对象 (但是静态成员函数没有)
-
如对函数
void create(int n, int d) { num = n; den = d;}
经过编译后,实际函数为
void create(int n, int d)
{ this->num = n; this->den = d;}*** this指针的使用 ***
-
一种情况就是,在类的非静态成员函数中返回类对象本身的时候,直接使用 return *this;
-
另外一种情况是当参数与成员变量名相同时,如this->n = n (不能写成n = n)。
-
全局函数,静态函数都不能使用this指针(因为都找不到那个具体的对象)
单个参数的时候可以直接赋值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6LiYtQK6-1620527632623)(C:\Users\hjkl\AppData\Roaming\Typora\typora-user-images\image-20210509091130544.png)]
拷贝构造函数
可以用已有的对象初始化
就是拷贝构造函数(遇到指针要自己写一个拷贝构造函数)
class Str{
int Length; char *Sp;
public:
Str(char *string){
if(string){Length=strlen(string);
Sp=new char[Length+1];
strcpy(Sp,string); }
else Sp=0;
}
void Show(void){cout<<Sp<<endl;}
~Str(){ if(Sp) delete []Sp; }
};
void main(void)
{ Str s1("Study C++");
Str s2(s1); //调用拷贝构造函数
s1.Show (); s2.Show ();
}
调用时机
- 对象定义时
- 函数调用时,把对象作为参数传给值传递的形式参数
- 把对象作为返回值时
自己设计一个字符串的类
class Str {
char* Sp;
public:
Str(char* string) {
if (string) {
Sp = new char[strlen(string) + 1];
strcpy_s(Sp, strlen(string)+1, string);
}
else Sp = 0;
cout << "调用构造函数" << endl;
}
Str(Str& s) {
if (s.Sp) {
Sp = new char[strlen(s.Sp) + 1];
strcpy_s(Sp, strlen(s.Sp) + 1, s.Sp);
cout << "调用拷贝构造函数" << endl;
}
else Sp = 0;
}
~Str() { if (Sp) delete[]Sp; }
};
void main(void)
{
char a[80] = "Study C++";
char* p;
p = a;
Str s1(p);
Str s2(s1); //调用拷贝构造函数
}
类的生命周期
消失顺序:
1、局部变量先消失,然后是静态局部变量, 最后是全局变量;
2、后创建的先消失;
const 对象
const 对象
- const对象的定义
const MyClass obj(参数表); - const对象不能被赋值,只能初始化,而且一定要初始化,否则无法设置它的值
- 常对象只能调用常成员函数,不能调用该对象的普通成员函数。
- 如果要修改常对象中某个数据成员,可以声明为(mutable int count )
- 任何不修改数据成员的函数都应该声明为const类型。如果在编写const成员函数时,不慎修改了数据成员,或者调用了其他非const成员函数,编译器将指出错误,这无疑会提高程序的健壮性。
class A { int x;
public:
A(int i) {x=i;}
int getx() const
{return x;}
};
int A::getx() const
{ return x; }
类外定义的时候,const 必须加,
但是static 的类外定义 不加static
- const数据成员的初始化只能在类构造函数的初始化表中进行,不能在构造函数中对他赋值。
- 例:
class A
{
A(int size); //构造函数
const int SIZE;
}
A::A(int size) : SIZE(size) //构造函数的初始化表
{…}
A a(100); //对象a的SIZE的值为100
A b(200); //对象b的SIZE的值为200
常成员函数
常成员函数可以引用const 数据成员也可以引用非const数据成员。 (只是不能改)
const 数据成员可以被常成员函数调用,或者非常成员函数调用
指向对象的常指针
将指针变量声明为const 型,这样指针变量始终保持为初值(指向),不能改变,其指向不变。
Time *const pt;
pt=&T1;
pt=&t2;//错误,不能改变指向
指向常对象的指针变量
const char * ptr;
const 类型名 *指针变量
const char *p;
p=c;
*p=‘jshd’; //错误;
*想在函数调用的时候不修改对象的值,可以把形参设置为const
void fun(const Time * p);
这样调用的时候是不允许修改值的
静态成员
- 静态成员变量不属于对象的一部分,而是类的一部分;
- 静态成员变量的初始化不能放在类的构造函数中;
- 类定义并不分配空间,空间是在定义对象时分配
- 但静态数据成员属于类,因此定义对象时并不为静态成员分配空间。
- 为静态成员分配空间称为静态成员的定义
- 静态成员的定义一般出现在类的实现文件中。如在SavingAccount类的实现文件中,必须要如下的定义:
double SavingAccount::rate = 0.05;
该定义为rate分配了空间,并给它赋了一个初值0.05。 - 如果没有这个定义,连接器会报告一个错误。
- 可以通过作用域操作符从类直接调用。如: SavingAccount::rate
- 但从每个对象的角度来看,它似乎又是对象的一部分,因此又可以从对象引用它。如有个SavingAccount类的对象obj,则可以用:obj.rate
- 由于是整个类共享的,因此不管用哪种调用方式,得到的值都是相同的
自定义一个数组类,一个字符串类
class Str{
char *Sp; int Length;
public:
Str(char *string)
{
if(string)
{
Length=strlen(string);
Sp=new char[Length+1];
strcpy(Sp,string);
}
else Sp=0;
}
void Show(void){ cout<<Sp<<endl; }
~Str() { if(Sp) delete []Sp; }
};
类的组合
在一个类中还有一个类
class A
{
float x,y;
public:
A(int a,int b) { x=a;y=b; }
void Show(){ cout<< "x="<<x<<'\t'<<"y="<<y<<'\n'; }
};
class C
{
float z;
A a1;//类C的数据成员为类A 的对象a1
public:
C(int a,int b,int c):a1(b, c) {z=a;}//类C的对象初始化
void Show(){cout<< “z="<<a<<'\n'; a1.Show();}
};
void main(void)
{ C c1(1, 2, 3 ); //对类C的对象初始化
c1.Show();
}
初始化要用参数列表
class A{
float x;
public:
A(int a)
{x = a;
cout <<“调用了A的构造函数\n”;}
~A()
{cout <<“调用了A的析构函数\n”;}
};
class B{
float y;
public:
B(int a)
{y = a;
cout <<“调用了B的构造函数\n”;}
~B()
{cout <<“调用了B的析构函数\n”;}
};
class C{
float z;
B b1;
A a1;
public:
C(int a, int b, int c): a1(a), b1(b)
{z = c;
cout <<“调用了C的构造函数\n”;}
~C()
{cout <<“调用了C的析构函数\n”;}
};
void
main(void)
{C
c1(1, 2, 3);}
继承
继承的定义
在C++中所谓“继承”就是在一个已存在的类的基础上建立一个新的类。
已存在的类称为“基类(base class)”或“父类(father class)”。新建立的类称为“派生类(derived class)”或“子类(son class)”
class Student1: public Student//声明基类是Student
{private:
int age; //新增加的数据成员
string addr; //新增加的数据成员
public:
void display_1( ) //新增加的成员函数
{ cout<<"age: "<<age<<endl;
cout<<"address: "<<addr<<endl;
}
};
—在C++语言中,一个派生类可以从一个基类派生,也可以从多个基类派生。从一个基类派生的继承称为单继承;从多个基类派生的继承称为多继承。----
- 但派生并不是简单的扩充,有可能改变基类的性质。
- 有三种派生方式:公有派生、保护派生、私有派生。
- 默认的是私有派生。
单继承的格式
一般格式:
class 派生类名:派生方法 基类名
{
//派生类新增的数据成员和成员函数
};
- protected成员是一类特殊的私有成员,它不可以被全局函数或其他类的成员函数访问,但能被派生类的成员函数访问
- protected成员破坏了类的封装,基类的protected成员改变时,所有派生类都要修改
基类成员在派生类中的访问特性
派生类对象的构造、析构与赋值操作
** 1.派生类的构造函数和析构函数
派生类要有自己的定义,也要给基类一个参数值(如果基类只有默认构造函数,或者不需要参数初始化,则构造函数可以省略)
** 派生类构造函数的格式:
派生类构造函数名(参数表):基类构造函数名(参数表)
{
}
-
基类构造函数中的参数表通常来源于派生类构造函数的参数表,也可以用常数值。
-
如果构造派生类对象时调用的是基类的缺省构造函数,则可以不要初始化列表。
顺序:
如果派生类新增的数据成员中含有对象成员,则在创建对象时,先执行基类的构造函数,再执行成员对象的构造函数,最后执行自己的构造函数体
*** 析构与构造函数调用时间相反***
基类有,则派生类必须也要;
重定义基类的函数
*** 注意区别重定义和重载 ***
重定义: 函数原型完全相同,派生类的函数会覆盖基类的函数。
重载: 函数名相同,但是原型不同
- 派生类引用基类的同名函数
引用基类的同名函数必须使用作用域运算符,否则会由于派生类成员函数实际上调用了自身而引起无穷递归。这样会使系统用光内存,是致命的运行时错误。
double area()
{ return 2 * circle::area() + circum() * height; }
double volumn() { return circle::area() * height ; }
派生类作为基类
基类本身可以是一个派生类,如:
class base { … }
class d1:public base {…}
class d2:public d1 {…}
每个派生类继承他的直接基类的所有成员。
- 如果派生类的基类是一个派生类,则每个派生类只负责他的直接基类的构造,依次上溯。
- 当构造d2类的对象时,会先调用d1的构造函数,而d1的构造函数执行时又会先调用base的构造函数。
- 因此,构造d2类的对象时,最先初始化的是base的数据成员,再初始化d1新增的成员,最后初始化d2新增的成员。
析构的过程正好相反。
将派生类对象隐式转换为基类对象
1.将派生类对象赋给基类对象
派生类中的基类部分赋给此基类对象,派生类新增加的成员就舍弃了。赋值后,基类对象和派生类对象再无任何关系。
class base {
public: int a;
};
class d1:public base{
public: int b;
};
在主函数中
d1 d;
d.a = 1; d.b = 2;
base bb = d; //派生类赋值给基类
cout << bb.a; //输出1
bb.a = 3;
cout << d.a; //输出1
例子
将派生类对象赋给基类对象
int i;
Point aPoint;
Circle aCircle;
Cylinder aCylinder;
Shape shapes[3]= {aPoint, aCircle, aCylinder};
for (i=0;i<3;i++) shapes[i].printShapeName();
2.基类指针指向派生类对象
-
尽管该指针指向的对象是一个派生类对象,但由于它本身是一个基类的指针,它只能解释基类的成员,而不能解释派生类新增的成员。因此,只能访问派生类中的基类部分。
-
通过指针修改基类对象时,派生类对象也被修改。
d1 d;
d.a = 1; d.b = 2;
base *bp = &d;
cout << bp->a; //输出1
Bp->a = 3;
cout << d.a; //输出3 -
int i;
Point aPoint;
Circle aCircle;
Cylinder aCylinder;
Shape *pShape[3]= { &aPoint, &aCircle,
&aCylinder };for (i=0;i<3;i++) pShape[i]->printShapeName();
3.基类的对象引用派生类的对象
给派生类中的基类部分取个别名。
基类对象改变时,派生类对象也被修改。
d1 d;
d.a = 1; d.b = 2;
base &bb = d;
cout << bb.a; //输出1
bb.a = 3;
cout << d.a; //输出3
例子
int i;
Point aPoint;
Circle aCircle;
Cylinder aCylinder;
Shape &shape1= aPoint;
shape1.printShapeName();
注意
- 不能将基类对象赋给派生类对象,除非在基类中定义了向派生类的类型转换函数
- 不能将基类对象 地址 赋给指向派生类对象的指针
- 也不能将指向基类对象的 指针 赋给指向派生类对象的指针。
- 如果程序员能确保这个基类指针指向的是一个派生类的对象,则可以用reinterpret_cast类型的强制类型转换。表示程序员知道这个风险