类和对象
C++引入了面向对象的设计方法,它是将数据及处理数据的相应函数封装到一个类中,类的实例称为对象.在一个对象中,只有属于该对象的函数才可以存取该对象的数据.
类的定义
- 类的定义分为声明部分和实现部分
- 声明部分:用来声明该类中的成员,包括数字成员(或称成员变量)的声明和成员函数(用来对数据成员进行操作,又称方法)的声明
- 实现部分:用来对成员函数进行定义.
- 格式:
class <类名>
{
private:
[<私有类型数据和函数>]
public:
[<公有型数据和函数>]
protected:
[<保护型数据和函数>]
};//不能省略该分号
<各个成员函数的实现>
- 其中
class
是定义类的关键字,class的后面是用户定义的类名(通常用大写C开始的标识符来描述,C代表Class一与对象,函数及其他数据类型名相区别). - 类中的关键字
public
,private
和protected
声明了类中的成员与程序其他部分(或类外)之间的关系,称为访问权限
public
:它们是公有的,能被外面的程序访问private
:它们是私有的,不能被外面的程序所访问.私有的数据成员只能由类中的函数所使用,私有的成员函数只允许在类中调用protected
,它们是受保护的,具有半公开性质,可在类中或其他子类中访问
<各个成员函数的实现>
是类定义中的实现部分.这部分包含所有在类体中声明的函数的定义
- 成员函数在类体中定义,则其实现部分不需要
- 成员函数在类的外部定义,必须有实现部分,必须由作用域运算符”::”来通知编译系统该函数所属的类
class CMeter
{
public:
double m_nPercent; // 声明一个公有数据成员
void StepIt(); // 声明一个公有成员函数
void SetPos(int nPos); // 声明一个公有成员函数
int GetPos() // 在类中定义成员函数
{
return m_nPos; // m_nPos是private型的变量,只能由类中的函数(这里是StepIt(),SetPos(),GetPos())所使用
}
private:
int m_nPos; // 声明一个私有数据成员
}; // 不要忘记分号
void CMeter::StepIt()
{
m_nPos++;
}
void CMeter::SetPos(int nPos)
{
m_nPos = nPos;
}
注意
* 类中的数据成员的类型可以是任意的(包括整型,浮点型,字符型,数组,指针,引用,另一类的对象)
* 不允许对所定义的数据成员进行初始化,也不能指定出static
外的任何存储类型
* 若成员前面没有任何访问权限(public,private,protected),则所定义的成员是private
(私有),这是类的默认设置
* 关键字public
,private
,protected
可以在类中多次出现,且前后的顺序没有关系.每个访问权限关键词为类成员所确定的访问权限是从该关键词开始到下一个关键词为止.
* 习惯将类的声明放在.h
文件中,将成员函数的实现放在与.h
同名的.cpp
文件中
对象的定义
类声明后,就可以定义该类的对象了
- 格式:
<类名> <对象名表>
其中类名是用户已定义过的标识符,多个对象名用逗号隔开,**被定义的对象可以是一个普通对象,也可以是数组对象后指针对象.
CMeter myMeter,*Meter,Meters[2];
- 一个对象的成员就是该对象的类所定义的数据成员(成员变量)和成员函数.访问对象的成员变量和成员函数,需要在成员前面加上对象名和成员运算符”
.
“,如下
<对象名>.<成员变量>
<对象名>.<成员函数>(<参数表>)
- 一个类对象只能访问该类的公有型成员,而对于私有型成员则不能访问
- 指针对象的访问:
<对象指针名>-><成员变量>
<对象指针名>-><成员函数>(<参数表>)
->
是一个表示成员的运算符,用来表示指向对象的指针的成员
* 以下两种表示是等价的
<对象指针名>-><成员变量>
(*<对象指针名>).<成员变量>
- 引用类型对象成员的访问形式与一般对象的访问形式相同
类作用域和成员访问权限
- 类的作用域:指在类的定义中由一堆花括号括起来的部分.
- 在类的作用域中,定义的变量不能使用
auto
,register
和extern
等修饰符,只能用static
修饰符,而定义的函数也不能用extern
修饰符
1.类名的作用域
- 如果在类声明时指定了类名,则类名的作用域范围是从类名指定的位置开始一直到文件结尾
- 如果在类声明之前就需要使用该类名定义对象,必须使用以下格式进行提前声明:
class <类名>;
如:
class COne; //将类COne提前声明
class COne; //可声明多次
class CTwo
{
//...
private:
COne a; // 数据成员a是已定义的COne类的对象
};
class COne
{
//...
};
2.类中成员的可见性
- 在类中使用成员时,成员声明的前后不会影响该成员在类中的使用,这是类作用域的特殊性
class A
{
void f1()
{
f2(); // 调用类中的成员函数f2
cout << a << endl; // 调用类中成员变量a
}
void f2(){}
int a;
};
2.由于类的成员函数可以在类体外定义,所以由“类名::”指定开始一直到函数体最后一个花括号为止的范围也是该类作用于的范围
3.在同一个类的作用域中,不管成员具有怎样的访问权限,都可在类作用域中使用,而在类作用域外不可使用
3.类外对象成员的可见性
- 对于访问权限
public
,private
,protected
来说,只有在子类或用对象来访问成员时,它们才会起作用 - 在用类外对象来访问成员时,只能访问
public
成员,而对private
和protected
均不能访问
构造函数和析构函数
- 一个类总有两种特殊的成员函数:构造函数和析构函数
- 构造函数:在创建对象时,使用给定的值将对象初始化
- 析构函数:用来释放一个对象,在对象删除前,用它来做一些内存释放等清理工作
1.构造函数
- 在类的定义中不能对数据成员进行初始化.为了能给数据成员设置某些初值,就要使用类的特殊成员函数—构造函数
- 构造函数会在对象建立时,被自动执行.用于变量,对象的初始化代码一般放在构造函数中
- C++规定,一个类的构造函数必须与相应的类同名,可以重载,也可以有默认的形参值.如:
class CMeter
{
public:
CMeter(int nPos) //带参数的构造函数
{
m_nPos = nPos;
}
// ...
};
// 这样,若有
CMeter oMeter(10);
// 会自动调用构造函数 CMeter(int nPos),从而给对象oMeter的私有成员m_nPos的值为10
2.对构造函数的几点说明
1.构造函数的约定使系统在生成类的对象时自动调用.指定对象括号里的参数就是构造函数的实参.当构造函数重载及设定构造函数默认形参值时,要避免出现二义.
2.定义的构造函数不能指向其返回值的类型,也不能指定为void类型.
3.若要用类定义对象,则构造函数必须是公有型成员函数,否则类无法实例化.若类仅用于派生其他类,则构造函数可定义为保护型成员函数.
3.默认构造函数
- 在类定义时,如果没有定义任何构造函数,则编译自动为类隐式生成一个不带任何参数的默认构造函数.该构造函数仅仅是为了满足对象创建时的语法需要.
注意: - 默认构造函数对数据成员初值的初始化还取决于对象的存储类型.
CMeter one; // 自动存储类型,数据成员的初值为无效值
static CMeter one; // 静态存储类型,数据成员的初值为空值或为0
- 若类定义中指定了构造函数,则隐式的默认构造函数将不再存在.
4.析构函数
- 析构函数只是在类名称前面加上”
~
“符号,以示与构造函数功能相反.每个类都有一个析构函数,没有任何参数,也没有返回值 - 调用:
- 当对象定义在一个函数体中,该函数调用结束后,析构函数被自动调用
- 用
new
为对象分配动态内存,但使用delete
释放对象时,析构函数被自动调用
- 注意:为了保证类的封装性,类中的指针成员所指向的内存空间必须在类中自行独立开辟和释放.
#include <iostream>
#include <string.h>
using namespace std;
class CName
{
public:
CName()
{
strName = NULL;
}
CName(char *str)
{
strName = (char *)new char[strlen(str)+1];
strcpy(strName,str);
}
~CName()
{
if(strName)
delete []strName;
strName = NULL;
}
char *getName()
{
return strName;
}
private:
char *strName;
};
int main()
{
char *p = new char[5];
strcpy(p,"DING");
CName one(p);
delete []p;
cout << one.getName() << endl;
return 0;
}
对象赋值和拷贝
1.赋值
- C++用下列形式的初始化作为将另一个对象作为对象的初值
<类名><对象名1 >(<对象名 2>)
CName o2("Ding");
CName o3(o2); // 将o2作为o3的初始值
- 事实上,o3的这种初始化形式还是要调用相应的构造函数—
CName(const CName &)
,这个特殊的默认构造函数称为默认拷贝构造函数
CName o1;
CName o2("Ding");
o1=o2;
/*
运行会终止,因为"CName o1;",编译会自动调用相应的默认的构造函数,所以strName为空,而"o1=o2;"中,C++赋值运算符的操作是将有操作对象的内容拷贝到左操作对象的内存空间中,由于左操作对象o1中的strName没有指向任何内存空间,因此试图将数据复制到一个不存在的内存空间中,程序必然异常终止,所以"o1=o2;"不可行.
*/
CName o2("Ding");
CName o3(o2);//等价于"CName o3 = o2;"
/*
也会出现程序终止.除非将strName改成int类型,因为int自身的内存空间就是用来存储数据的,而"char *"还需要另外开辟一个内存空间用来存储数据.
*/
2.浅拷贝和深拷贝
- 浅拷贝:仅仅将内存空间的内容拷贝的方式称为浅拷贝.默认构造函数是浅拷贝方式.
- 深拷贝:在进行数值拷贝之前,为指针类型的数据成员另辟一个独立的内存空间.
3.深拷贝构造函数
- 定义:
<类名>(参数表){}
课件,拷贝构造函数的格式就是带参数的构造函数. - 拷贝操作的实际是类对象空间的引用
- C++规定,拷贝构造函数的参数个数可以是1个或多个,但左起第一个参数必须是类的引用对象.可以是
类名 &对象
,或const 类名 &对象
- 一旦定义了拷贝构造函数,则隐式的默认拷贝构造函数和隐式的默认构造函数就不再生效了.
#include <iostream>
#include <string.h>
using namespace std;
class CName
{
public:
CName()
{
strName = NULL;
}
CName(const char *str) // 这里必须为const型,否则会报错
{
strName=(char *)new char[strlen(str)+1];
strcpy(strName,str);
}
CName(CName &one)
{
//为strName开辟独立的内存空间
strName = (char *)new char[strlen(one.strName)+1];
strcpy(strName,one.strName); //复制内容
}
CName(CName &one,const char *add)
{
// 为strName开辟独立的内存空间
strName = (char *)new char[strlen(one.strName)+1];
strcpy(strName,one.strName); // 复制内容
strcat(strName,add); // 连接到staName中
}
~CName()
{
if(strName) delete []strName;
strName = NULL; // 是一个好习惯
}
char *getName()
{
return strName;
}
private:
char *strName; // 字符指针
};
int main()
{
CName o1("Ding"); // 通过构造函数初始化
CName o2(o1); // 通过显式的默认拷贝构造函数来初始化
cout << o2.getName() << endl;
CName o3(o1,"YOU HE"); // 通过带其他参数的拷贝构造函数来初始化
cout << o3.getName() << endl;
return 0;
}
对象成员的初始化
- 在一个类的数据成员中,除了普通数据类型变量外,还往往是其他已定义的类的对象,这样的成员称为对象成员
- 组合类:拥有对象成员的类
- C++允许在构造函数的函数头后面跟一个由冒号”:”来引导的对象成员初始化列表,列表中包含类中对象成员或数据成员的拷贝初始化代码,各对象初始化之间用逗号分隔
- 格式:
<类名>::<构造函数名>(形参表):对象1(参数表),对象2(参数表),...,对象n(参数表){}
- 方式:
- 函数构造方式:在构造函数体中进行
- 对象成员列表方式:由冒号”:”来引导的对象成员初始化
#include <iostream>
using namespace std;
// 第一种方式:函数构造方式
class CPoint
{
public:
CPoint(int x,int y)
{
xPos = x;
yPos = y;
}
private:
int xPos,yPos;
};
class CRect
{
public:
CRect(int x1,int y1,int x2,int y2)
{
m_ptLT = CPoint(x1,y1);
m_ptRB = CPoint(x2,y2);
}
private:
CPoint m_ptLT,m_ptRB;
};
int main()
{
CRect rc(10,100,80,250);
return 0;
}
/*
出现:找不到匹配的CPoint默认构造函数"的编译错误
原因:运行到`CRect rc(10,100,80,250);`的时候,编译器将要扫描CRect的成员,所以会先为它的private成员m_ptLT,m_ptRB分配空间,
但是由于CPoint已经定义了有参的构造方法,所以没有了默认的无参构造方法,因此出现编译错误
*/
#include <iostream>
using namespace std;
// 第二种方式:对象成员列表方式
class CPoint
{
public:
CPoint(int x,int y)
{
xPos = x;
yPos = y;
}
private:
int xPos,yPos;
};
class CRect
{
public:
CRect(int x1,int y1,int x2,int y2)
:m_ptLT(x1,y1),m_ptRB(x2,y2)
{
}
private:
CPoint m_ptLT,m_ptRB;
};
int main()
{
CRect rc(10,100,80,250);
return 0;
}
/*
编译顺利通过
当定义了rc对象时,编译器首先扫描CRect类,为它的成员分配空间,即为m_ptLT,m_ptRB分配内存空间,然后从对象初始化
列表中寻找其初始化代码,然后根据对象成员的初始化形式调用相应的构造函数进行初始化.找不到则调用相应的构造函数进行初始化.
*/
注意:
* 函数构造方式,实际上是将对象成员进行了两次初始化:第一次是在对象成员声明的同时自动调用默认构造函数进行的,而第二次是在构造函数体中执行的初始化代码
* 对象成员列表,虽然是将对象成员的定义和初始化代码放在两个地方书写,但是却是同时运行,因此对象初始化列表的形式能简化对象初始化操作,提高对象初始化效率.
* 在对象成员列表下,成员初始化的顺序是按成员的声明次序进行的,与成员在由冒号”:”来引导的对象初始化列表中的次序无关.