8 面向对象
public成员是类的接口
8.2.2 类的定义
- 数据成员的类型符前不可使用auto、extern和register[变量存储位置和作用域,类和结构体中的变量是成员变量,其存储位置和作用域由定义对象的函数决定,不由对象本身决定],也不可在类定义时对数据成员初始化
- 类定义中提供的成员函数时函数的原型声明。
8.3 C++类的实现
一种是在类定义时完成成员函数的定义,二是在类定义的外部定义其成员函数
8.5 对象的作用域、可见域和生存周期
8.5.3 构造函数支持重载
定义有参构造函数和无参构造函数
8.5.4 构造函数允许按参数默认方式调用
如果仅仅是参数个数不同,推荐使用参数默认方式
8.5.5 初始化表达式
由逗号分隔的数据成员组成,初值放在一对圆括号内。只要将成员初始化表达式放在构造函数的头体之间,并用冒号将其与函数头分隔开,可实现数据成员表达式元素的初始化。在构造函数体之前进行
初始化表中的初始化顺序是由成员在类中被声明的顺序决定的。
A(int a,int b):x(a),y(b)
{}
8.5.6 析构函数
默认析构函数只清除类的数据成员所占据的空间,但**对类的函数成员通过new和malloc 动态申请的内存无能为力,应在类的析构函数中通过delete或free进行释放,这样能有效避免对象撤销造成的内存泄露**。
8.6 复制构造函数
系统默认一个复制构造函数,它是一个inline或public 的成员函数,其函数原型
point::point(const point &);
8.6.2 默认复制构造函数
赋值操作后,指针指向同一内存,当一个对象析构后 ,会导致重复释放内存和野指针问题
8.6.3 显式定义复制构造函数
如果类中含有指针型的数据成员、需要使用动态内存,程序员最好显式定义自己的复制构造函数,避免各种可能出现的内存错误。
class computer
{
char *brand;
float price;
computer(const computer &cp)
{
brand=new char[strlen(cp.brand)+1];/*重新为brand开辟cp.brand大小的动态内存*/
strcpy(brand,cp.brand);/*字符串复制*/
price=cp.price;
}
};
8.7 特殊数据成员1
8.7.1 const 数据成员
只能通过成员初始化表达式进行初始化。编译器提供的默认构造函数无法完成对const成员的初始化,而**缺省的复制构造函数可以完成const成员的初始化**
#include<iostream>
using namespace std;
class point
{
const int xPos;
const int yPos;
public:
point(const int x=0,const int y=0):xPos(x),yPos(y)
{
cout<<"调用构造函数"<<endl;
}
void print()
{
cout<<"y="<<yPos<<"x="<<xPos<<endl;
}
};
int main()
{
point p1(3,4);
p1.print();
point p2(p1);//缺省的复制构造函数可以完成const成员的初始化
p2.print();
return 0;
}
8.7.2 引用成员
只能通过成员初始化表达式来进行初始化,不可使用默认构造函数,可使用默认复制构造函数。当类中含有引用类型的数据成员(尤其是引用本对象的成员时),不要使用默认的复制构造函数,应当显式地给出复制构造函数,避免程序出现无法预料的错误。
8.7.3 类对象成员
类对象的私有数据成员xPos和yPos都是private成员,只能在pt1、pt2初始化表达式中对类进行初始化
#include<iostream>
using namespace std;
class point
{
const int xPos;
const int yPos;
public:
point(const int x=0,const int y=0):xPos(x),yPos(y)
{
cout<<"调用构造函数"<<endl;
}
void print()
{
cout<<"y="<<yPos<<" x="<<xPos;
}
point(const point&p):xPos(p.xPos),yPos(p.yPos)
{
cout<<"调用复制构造函数"<<endl;
}
~point()
{
cout<<"调用析构函数"<<endl;
}
};
class line
{
point pt1;
point pt2;
public:
line(const int x1,const int y1,const int x2,const int y2):pt1(x1,y1),pt2(x2,y2)
{
cout<<"线的构造函数"<<endl;
}
line(const line &l):pt1(l.pt1),pt2(l.pt2)
{
cout<<"线的复制构造函数"<<endl;
}
void drow()
{
pt1.print();
cout<<" to ";
pt2.print();
cout<<endl;
}
~line()
{
cout<<"线的析构函数"<<endl;
}
};
int main()
{
line l1(1,2,3,4);
l1.drow();
line l2(l1);
l2.drow();
return 0;
}
8.7.5 static 数据成员
使用static修饰数据成员,成员在编译时就被创建并初始化(在定义性声明时被创建的)(与之相比,对象是在运行时才被创建的)
静态数据成员,应在类声明之外使用单独的定义性声明语句完成其初始化,该语句不能再使用static
初始化一定放在cpp文件中,不能放在h 文件中,因为h 文件会出现在多个cpp 文件中,出现初始化的复本,引发错误。
静态成员使用const修饰(const与static没有前后之分),而且是整型、浮点型、布尔型或枚举型,但不能是类对象、数组、引用和指针,C++允许该成员在类定义中初始化,这样,便不能在外部再次对该成员进行定义性声明,但对该成员的引用性声明是允许的。
8.8 特殊函数成员
接口:public
内部实现:private
8.8.1 静态成员函数
如果需要在静态函数成员内访问类的非静态成员,需要将对象引用或指向对象的指针作为参数。既可定义在类内,也可定义在类外,定义在类外不使用static。
不能用const修饰的原因:
一个静态成员函数访问的值是其参数、静态数据和全局变量,这些数据都不是对象状态的一部分。对成员函数用const修饰是为了表明函数并不会修改访问对象的数据成员。既然一个静态成员函数根本不访问非静态数据成员,那么没必要使用const
class computer
{
char *brand;
float price;
computer(const computer &cp)
{
brand=new char[strlen(cp.brand)+1];/*重新为brand开辟cp.brand大小的动态内存*/
strcpy(brand,cp.brand);/*字符串复制*/
price=cp.price;
}
/*访问类的非静态成员,需要将对象引用或指向对象的指针作为参数*/
static void print(computer &p)
{
cout<<p.price<<endl;
}
};
8.8.2 const成员函数
把const 关键字放在函数的参数表和函数体之间,表示该成员函数只能读取类的数据成员,不能修改类成员数据,const成员函数不能调用另一个非const成员函数或者改变该类的数据成员。
任何不修改成员数据的函数都应该声明为const函数,这有助于提高函数的可读性和可靠性
class computer
{
char *brand;
float price;
computer(const computer &cp)
{
brand=new char[strlen(cp.brand)+1];/*重新为brand开辟cp.brand大小的动态内存*/
//strcpy(brand,cp.brand);/*字符串复制*/
price=cp.price;
}
void print(computer &p) const
{
cout<<p.price<<endl;
}
};
8.9 对象的组织
可以const对象来创建指向对象的指针和创建对象数组,还可以使用new和delete等创建动态对象。
8.9.1 const对象
能作用于const对象的成员函数除了构造函数和析构函数外,只有const成员函数,因为const对象只能被创建、撤销以及只读访问,不许改写。
8.9.3 对象的大小
对象的大小一般是类中所有非static成员的大小之和。
- C++将类中的引用成员当作“指针”来维护,占4个内存字节。
- 类中有虚函数时(虚析构函数除外),还会分配一个指针用来指向虚函数表,因此,加4个字节
8.9.4 this指针
编译器不会向静态成员函数传递this指针,这既是“当静态函数访问类的非静态成员时,需要将对象的引用或指向对象的指针作为参数”的原因
this指针的作用
- 显式指明类中数据成员,尤其是和形参以及全局变量相区别
- 返回本对象的指针或引用
8.10 可变参数
9.1 类的作用域
访问类的成员数据x,有两种途径:一是使用“类名::数据成员名”(也可访问嵌套类),二是使用this指针(不可用this指针访问嵌套类)
使用“::x”可以在程序的任何地方访问全局变量。
9.2 类定义的作用域与可见域
2.类作用域
一个类可以定义在另一个类中,这是所谓的嵌套类。若类A定义在类B中,A的访问权限为public 则A的作用域可以认为和B相同,不同之处是在于必须使B::A的形式访问A的类名。当然,若A的访问权限是private,则只能在类内使用类名来创建该类的对象,无法在类外创建A类的对象。嵌套类的成员函数既可以定义为inline也可定义在类外,合理使用作用域限定符"::".
9.3 对象的生存期、作用域和可见域
9.3.1 先定义后实例化
不创建对象,仅仅声明一个指向类型B对象的指针
class B;/*引用性声明*/
B* pB=NULL;
9.4 友元
可以定义一个函数、类为类的友元,则友元可以访问类的私有成员
9.4.1 友元的非成员函数
在某个类的定义中用friend声明一个外部函数(或者其他类的函数成员,可以是public也可private)后,这个外部函数称为类的友元。
特点:
- 类内只需对函数进行声明,声明的位置没有要求
- 函数定义要放在类外,具体定义位置没有要求
- 友元函数不是类的成员函数,在实现时和普通函数一样,在实现时不用“::”指示属于那个类
- 一个函数可以同时作为多个类的友元函数
9.4.2 友元的成员函数
当A类的成员函数作为B类的友元函数时,必须先定义A类,而仅仅是声明它,对其实现没有具体要求。
9.4.3 友元函数的重载
使得一组重载函数全部成为类的友元,必须一一声明,否则只有匹配的那个函数会成为类的友元,编译器仍将其他函数当作普通函数来处理。
9.4.4 友元类
类A作为类B的友元,类A 先声明,B类定义后A类再定义。
- 友元关系是单向的
- 友元关系不具有传递性
- 友元关系不被继承
9.5 运算符重载
operator是C++的一个关键字,它和运算符一起使用,表示一个运算符重载函数,在理解时可将operator和运算符视为类的一个成员函数1。
对双目运算符来说,编译器将左边对象解释为调用对象,将右边对象解释为传递给运算符的参数。cx1+cx2等价于cx1.operator+(cx2)
complex& complex::operator ++()/*解释为对象.operator++(),其他前置单目运算符与此类似*/
{
cout<<"前置++"<<endl;
real+=1;
imag+=1;
return (*this);/*返回自身引用(*this)使得++对象 可以作为左值*/
}
complex complex::operator ++(int)/*解释为对象.operator(0),其他后置单目运算符与此类似*/
{
cout<<"后置++"<<endl;
complex ctemp=*this;
++(*this);
return ctemp;/*以传值形式返回,不能成为左值*/
}
将操作符定义为友元函数的形式可让程序更容易实现类型的自动转换,使两个操作符都被当作函数的参数。
9.6 运算符重载
9.6.1 赋值运算符
class obj1=obj2;/*调用类的复制构造函数,完成obj1的创建并初始化*/
class obj1;/*先调用obj1的无参构造函数(或所有参数都有默认值的构造函数)完成obj1的创建*/
obj1=obj2;/*调用赋值运算符将obj2所有的成员的值复制到obj1*/
/*问题在于当class中包含指针指向动态内存时,会导致两个对象的指针指向同一块内存,导致其中一个对象的指针变为野指针*/
/*解决方法:将复制构造函数的特殊操作等同定义在赋值运算符重载中*/
class computer
{
private:
char *brand;
float price;
/*复制构造函数*/
computer(const computer&p)
{
price=p.price;
brand=new char[strlen(p.brand)+1];
if(brand!=NULL)
{
strcpy(brand,p.brand);
}
cout<<"复制构造函数被调用"<<endl;
}
computer& operator=(const computer &p)
{
/*
忽视的几点问题
1.判断是否为自赋值。自己给自己赋值没有意义,如果不加判断就为指针重新申请内存,原来所指向的动态内存就泄露了
2.释放brand所指向的内存,delete一个NULL指针是不会出现问题的。为了有效防错,delete后就立即将brand置为NULL
*/
if(this==&p)/*如果时自赋值返回当前对象*/
{
return (*this);
}
price=p.price;
/*防止内存泄露,先释放brand(不是p.brand)指向的内存*/
delete [] brand;
brand=new char[strlen(p.brand)+1];
if(brand!=NULL)
{
strcpy(brand,p.brand);
}
return (*this);/*返回当前对象的引用,为的是实现链式赋值*/
}
}
9.6.2 函数调用运算符
函数调用运算符同样只能重载为成员函数形式
function(arg1,arg2,...)
function.operator()(arg1,arg2,...)/*其作用是将函数调用运算符()作用在对象function上,不过其参数并没有个数限制*/
一个类如果重载了函数调用符,可以将对象作为一个函数使用,这样的类的对象又称为函数对象,函数也是一种对象
#include<iostream>
using namespace std;
/*一个类如果重载了函数调用符,可以将对象作为一个函数使用,这样的类的对象又称为函数对象,函数也是一种对象*/
class Demo
{
public:
double operator()(double x,double y);
double operator()(double x,double y,double z);
};
double Demo::operator()(double x,double y)
{
cout<<"1:";
return x>y?x:y;
}
double Demo::operator()(double x,double y,double z)
{
cout<<"2:";
return (x+y)*z;
}
int main()
{
Demo de;
cout<<de(1.0,2.3)<<endl;
cout<<de(1.0,2.0,2.5)<<endl;
return 0;
}
9.6.3 下标运算符
返回引用类型很关键,这使得返回值可以作为左值
返回类型& operator[](参数类型);/*返回引用类型很关键,这使得返回值可以作为左值*/
#include<iostream>
using namespace std;
class Demo
{
int len;
char* pBuf;
public:
Demo(int l)
{
len=l+1;
pBuf =new char[len];
}
~Demo()
{
delete [] pBuf;
cout<<"调用析构函数"<<endl;
}
int GetLen()
{
return len;
}
char& operator[](int i)
{
static char def='\0';
if(i<len&&i>=0)
return pBuf[i];
else
{
cout<<"下标越界"<<endl;
return def;
}
}
};
int main()
{
Demo de(5);
char *sz="hello";
for(int i=0;i<strlen(sz);i++)
de[i]=sz[i];
for(int i=0;i<de.GetLen();i++)
cout<<de[i];
cout<<endl;
return 0;
}
9.7.1 由其他类型向定义类的转换
能接受一个参数的构造函数称为转换函数,多出来的参数必须有默认值
point(anotherpoint ap);/*p1=p2隐式转换可用*/
explicit point (anotherpoint ap);/*p1=p2隐式转换不可用,只能p1=point(p2);才能完成*/
9.7.2 由自定义类向其他类型的转换
自定义类型是用户定义的强制类型转换函数,如何创建强制类型转换函数呢?需要在类中定义如下形式的转换函数
推荐使用显式转换
operator 目标类型名()
{
return (目标类型的构造);
}
- 转换函数必须是成员函数,不能是友元函数形式
- 转换函数不能指定返回类型,但在函数体内必须用return语句以传值方式返回一个目标类型的变量
- 转换函数不能有参数
-9.8.2 完全匹配
完全匹配允许的不一致
实参 | 形参 |
---|---|
type | type& |
type& | type |
type[] | type* |
返回类型 函数名(参数列表) | 返回类型 (*指针)(参数列表) |
返回类型 (*指针)(参数列表) | 返回类型 函数名(参数列表) |
type | const type |
type* | const type* |
type | volatile type |
type* | volatile type* |
多个完全匹配的优选
- 指向非const变量或对象的指针或引用优先于const 的
多个完全匹配的优选 - 指向非const变量或对象的指针或引用优先于const 的
- 非模板函数优先于模板函数