1、类和对象
- 对象:现实中对象的模拟,具有属性(数据成员)和行为(函数成员),对象是类的实例
- 类:同一类对象的共同属性和行为
- 定义对象时,通过构造函数初始化
- 删除对象时,通过析构函数释放资源
2、面向对象程序设计的基本特点:抽象、封装、继承和多台
抽象
- 对同一类对象的共同属性和行为进行概况,形成类。
- 首先注意问题的本质及描述,其次是实现过程或细节
- 数据抽象:描述某类对象的类型和状态
- 代码抽象:描述某类对象的共有的行为特征或具有的功能
- 抽象的实现:类
抽象实例
封装
- 将抽象出来的数据成员、代码成员相结合,将他们视为一个整体
- 目的:增强安全性和简化编程,使用者不必了解具体的实现细节,只需要通过外部的接口,以特定的访问权限,来使用类的成员。
- 实现封装:类声明中的{}
public:类的对外访问接口
private:在类的外面是看不到的,只有类的对象可以访问
继承
- 在已有类的基础上,进行扩展形成新的类
多态
- 多态:同一名称,不同功能实现方式
- 目的:达到行为标识统一,减少程序中标识符的个数
3、 类和对象的定义
定义类的语法形式
class 类名称
{
public:
公有成员(外部接口)
private:
私有成员
public:
保护性成员
}
类内初始值
- 当构造函数没有对数据成员进行初始化,那么就会用类内初始值进行初始化
- 为数据成员设置类内初始值
-
用于初始化函数成员
类成员的访问控制
- 公有类型成员
- 私有类型成员
-
保护类型成员
4、定义类的对象
- 语法
类名 对象名; - 类中的成员之间直接使用,成员名互相访问
- 类外访问类的成员使用 “对象名.成员名” 方式访问pulic成员
- 类外不能访问类的private成员,类内随便访问
5、类的成员函数
成员函数的实现
- 语法
返回值类型 类名::函数名(){}
成员函数
- 在类中声明函数原型
- 可以在类外给出函数体的实现,并在函数名前使用类名加以限定
- 也可以直接在类中给出函数体,形成内联成员函数
- 允许声明重载函数和带默认参数值的函数(一般是构造函数)
- 类的成员函数不占用空间,只有数据成员才会占用空间,一般我们说的空间指的是存储空间,内存分成存储空间和代码空间,而成员函数放在了代码空间
内联成员函数
- 为了提高运行时的效率,对较简单的函数可以声明为内联形式
- 内联函数体中不要有复杂结构(如循环语句和switch语句)
- 在类中声明内联成员函数的方式
- 将函数体放在类的声明中(构造函数通常写成内联函数的形式)
- 在类外 使用inline ,格式为:inline 返回值类型 函数名(形参){}
6、构造函数
- 类中特殊函数
- 用于描述初始化算法
构造函数的作用
在对象被创建时使用特定的值构造对象
例如:
希望在构造一个Clock类对象时,
将初始时间设置为0:0:0,
就可以通过构造函数来设置。
构造函数的形式
- 函数名与类名相同
- 不能定义返回值类型,也不能有return语句
- 可以有形式参数,也可以没有形式参数
- 可以是内联函数
- 可以重载
- 可以带默认参数值(调用时,如果给出实参那么用实参的值,如果没有实参,那么就用默认参数值)
构造函数的调用时机
- 在对象创建时被自动调用
- 例如:
Clock myClock(0,0,0)
默认构造函数
- 调用时可以不需要实参的构造函数
- 参数表为空的构造函数
- 全部参数都有默认值的构造函数
下面两个全都是默认构造函数,但是不能在类中同时出现(因为在调用时,都不给实参,编译器无法判断调用哪一个)
//声明
Clock();//没有形参,参数表为空
Clock(int newH=0,int newM=0,int newS=0);//有形参,但是形参都是默认值,所以实参可以省略
//定义
Clock::Clock( )
{
一些初始化操作
cout << "Object is being created" << endl;
}
调用上面两个构造函数,都可以不给出实参。
隐含生成的构造函数
在建立新的对象时,一定会调用构造函数,不声明任何构造函数的话,编译器会自动帮我们生成一个默认构造函数
隐含生成的构造函数:
- 参数列表为空,不能为数据成员设置初始值
- 如果类内定义了成员的初始值,则使用内类定义的初始值
- 如果没有定义类内的初始值,则以默认方式初始化
- 基本类型的数据默认初始化的值是不确定的
总的来说就是隐含生成的构造函数,不会为数据成员初始化,如果成员在类内初始化了,那么就用那么值,如果没有,就用默认的方式初始化(初始化的值是不确定的)
#default
如果程序中已定义构造函数,默认情况下编译器就不再隐含生成默认构造函数,但此时如果仍希望系统生成默认生成的构造函数就可以使用"=default",这样就有两个构造函数可以使用了,用户自己生成的构造函数仍然可以使用
// use of defaulted functions
#include <iostream>
using namespace std;
class A {
public:
// user-defined
A(int x){
cout << "This is a parameterized constructor";
}
//Using the default specifier to instruct
// the compiler to create the default implementation of the constructor.
A() = default;
};
int main(){
A a; //call A()
A x(1); //call A(int x)
cout<<endl;
return 0;
}
带参数的构造函数
默认的构造函数(即使有形参,形参也都有默认值)和隐含生成的构造函数都没有参数,在创建对象时不需要任何参数,但是如果需要构造函数也可以带参数,这样在创建对象时就会给对象赋初始值,例子如下
class Line
{
public:
...
Line(double len); // 这是构造函数
private:
double length;
};
// 构造函数定义
Line::Line( double len)
{
cout << "Object is being created, length = " << len << endl;
length = len;
}
// 程序的主函数
int main( )
{
Line line(10.0);
return 0;
}
类中可以同时存在两个构造函数,一个默认构造函数(没有形参)由用户或者系统生成;一个带参数的构造函数。当用户自己定义了构造函数,系统将不会再隐含生成的默认构造函数,不过可以用 =default让系统生成
7、在程序中定义和使用构造函数
初始化列表
- 与其他函数不同,构造函数除了有名字,参数列表和函数体之外,还可以有初始化列表,初始化列表以冒号开头,后跟一系列以逗号分隔的初始化字段
- 初始化列表中赋值不能用等号A=B 而是要用括号A(B)
- 从概念上来讲,构造函数的执行可以分成两个阶段,初始化阶段和计算阶段,初始化阶段先于计算阶段
例1 带有参数的构造函数
class Clock{
public:
Clock(int newH,int newM,int newS);//带有参数的构造函数
void setTime(int newH,int newM,int newS);
void showTime();
private:
int hour,minute,second;
}
//构造函数的实现
Clock::Clock(int newH,int newM,int newS):
hour(newH),minute(newM),second(newS){}
// : 初始化列表 {
//如果对类的对象进行简单初始化,首选这种方式
//等价于hour=newH; minute=newM; second=newS;
...
int main(){
Clock c(0,0,0);//自动调用构造函数
}
例 2带有参数的构造函数和默认构造函数
class Clock{
public:
Clock(int newH,int newM,int newS);//带有参数的构造函数
Clock();//构造函数的重载
void setTime(int newH,int newM,int newS);
void showTime();
private:
int hour,minute,second;
}
//构造函数的实现
Clock::Clock(int newH,int newM,int newS):
hour(newH),minute(newM),second(newS){}
//默认构造函数的实现
Clock::Clock():hour(0),minute(0),second(0){}
//将hour minute second的值都设置为0
/* : 初始化列表 {
如果对类的对象进行简单初始化,首选这种方式
hour=newH
minute=newM
second=newS
*/
...
int main(){
//创建对象时后面有参数,调用有参数的构造函数,没参数时,调用无参数/默认构造函数
Clock c1(8,10,0);//自动调用构造函数 8,10,0
Clock c2;//自动调用构造函数 0,0,0
}
8、委托构造函数
什么是委托构造函数?
一个构造函数可以委托另一个构造函数帮它完成初始化功能
委托构造函数好处:
- 不需要反复写构造函数
- 保持代码实现的一致性,修改构造函数时,连同委托它实现的构造函数一并修改
例子:
//构造函数的实现
Clock::Clock(int newH,int newM,int newS):
hour(newH),minute(newM),second(newS){}
//默认构造函数的实现
Clock::Clock():hour(0),minute(0),second(0){}
原本的构造函数可以用委托构造函数来实现
//构造函数的实现
Clock::Clock(int newH,int newM,int newS):
hour(newH),minute(newM),second(newS){}
//用委托构造函数实现默认构造函数
Clock::Clock():Clock(0,0,0){}
9、复制构造函数
- 定义:复制构造函数是一种特殊的构造函数,其形参为本类的对象引用。
- 作用:是用一个已存在的对象去初始化同类型的新对象。
复制构造函数的声明以及实现
class 类名{
public:
类名(形参);//构造函数
类名(const 类名 &对象名);//复制构造函数,
//在引用前面加const是为了防止引用的双向传递造成一些错误,
//所以加const使引用变成了单向传递
...
};
//定义
类名::类名(const 类名 &对象名)//复制构造函数的实现
{
//函数体
}
复制构造函数被调用的三种情况(什么时候会用到复制构造函数)
- 定义一个对象时,以本类的另一个对象作为初始值,发生复制构造;(赋值)
- 如果函数的形参是类的对象,调用函数时,将使用实参对象初始化形参对象,发生复制构造(作形参)
- 如果函数的返回值是类的对象,函数执行完成后返回主调函数时,将使用return语句中的对象初始化一个临时无名对象,传递给主调函数,此时发生复制构造。(作返回值)
隐含的复制构造函数
- 如果程序员没有为类声明拷贝初始化(复制)构造函数,则编译器自己生成一个隐含的复制构造函数。
- 这个隐含的复制构造函数的功能是:用初始值对象的每个数据成员,初始化将要建立的对象的对应数据成员 。
- 一般情况下,编译器自己生成的隐含构造函数就够用,如果类的成员中有指针,很多情况下默认的隐含构造函数实现的浅层的复制构造就不够用了,需要做深层的复制构造
- 当需要做特定功能的复制构造(只复制一部分/修改一些),也需要自己实现复制构造函数
#delete
如果不希望对象被复制构造
C++98做法:将复制构造函数声明为private,且不提供函数的实现
C++11做法:用“#delete”指示编译器不生成默认复制构造函数
using namespace std;
class Point
{
public:
Point(int xx = 0, int yy = 0) { x = xx; y = yy; };
Point(Point& p);
int getX() { return x; };
int getY() { return y; };
private:
int x, y;
};
Point::Point(Point& p)
{
x = p.x;
y = p.y;
cout << "calling the copy constructor" << endl;
}
void fun1(Point p)
{
cout << p.getX() << endl;
}
Point fun2()
{
Point a;
return a;
}
int main()
{
Point a;
Point b(a);
cout << b.getX() << endl;
fun1(b);
b = fun2();
cout << b.getX() << endl;
return 0;
}
10 、析构函数
- 析构函数的作用:析构函数完成对象被删除前的一些清理工作(例如删除new开辟的空间)
- 调用时机:在对象的生存期结束的时刻,系统自动调用析构函数
- 默认析构函数:如果程序中未声明析构函数,编译器将自动产生一个默认的析构函数,其函数体为空(可以理解成什么都不做的函数)
- 析构函数的原型: ~类名();
- 析构函数没有参数,没有返回类型,也不允许有return语句
例子:
11、类的组合
类的组合:一些类的对象可以作为另外类的部件
组合的概念
- 类中的成员是另一个类的对象
- 可以在已有抽象的基础上实现更复杂的抽象。
构造函数如何实现?
组合类没有权利访问部件对象的内部私有成员,让部件对象调用自己的构造函数
实现:
组合类的构造函数负责将部件对象初始化所需要的参数传递给他,编译器会自动调用部件类的构造函数来初始化这些部件对象
类组合的构造函数设计
- 原则:不仅要负责对本类中的基本类型成员初始化,也要对对象成员初始化。
- 类组合构造函数声明及定义形式:
类名:类名(对象成员所需要的形参,本类成员形参):对象1(参数),对象2(参数),....{
//函数体其他语句
}
例如
//Line类中成员初始化都不需要参数
Line::Line(Point xp1, Point xp2) : p1(xp1), p2(xp2) {
cout << "Calling constructor of Line" << endl;
double x = static_cast<double>(p1.getX() - p2.getX());
double y = static_cast<double>(p1.getY() - p2.getY());
len = sqrt(x * x + y * y);
}
构造组合类对象时的初始化次序
先部件对象初始化构造,然后再是本来进行构造。
-
在部件对象初始化中,初始化顺序是根据部件对象在类中声明的顺序,而不是部件对象在列表中初始化的顺序。
在组合类的构造中有特殊的情况,比如有些部件对象没有在构造函数的初始化列表中,那么该部件对象的默认构造函数将会被执行。记住这是默认构造函数,如果我们自己重新定义了默认构造函数,那么编译器生成的默认构造函数就不存在了,而调用我们自己定义的默认构造函数,并且这个构造函数不能带参数(有参数也必须都是默认值)
需要注意的是,有些数据成员的初始化必须要构造函数的初始化列表中进行。一类是没有默认构造函数的部件对象,因为这类对象初始化时必须提供参数(为什么必须提供参数,因为这类对象只有带参数的构造函数,没有默认构造函数,而编译器也不会为他生成构造函数,如果这类对象不在初始化列表中,就会报错,无法初始化),另一类是引用类型的数据成员——因为引用型变量必须在初始化时绑定引用的对象。
组合类的代码实例
#include <iostream>
#include <cmath>
using namespace std;
class Point {
public:
Point(int xx = 0, int yy = 0) {
x = xx;
y = yy;
}
Point(Point& p);
int getX() { return x; }
int getY() { return y; }
private:
int x, y;
};
Point::Point(Point& p) {
x = p.x;
y = p.y;
cout << "Calling the copy constructor of Point" << endl;
}
class Line {
public:
Line(Point xp1, Point xp2);
Line(Line& l);
double getLen() { return len; }
private:
Point p1, p2;
double len;
};
//利用了对象p1和p2的复制函数对p1进行初始化
Line::Line(Point xp1, Point xp2) : p1(xp1), p2(xp2) {
cout << "Calling constructor of Line" << endl;
double x = static_cast<double>(p1.getX() - p2.getX());
double y = static_cast<double>(p1.getY() - p2.getY());
len = sqrt(x * x + y * y);
}
//复制构造函数,将对象l的l.p1赋给当前对象的p1,l.p2赋给当前对象的p2
Line::Line(Line& l) : p1(l.p1), p2(l.p2) {
cout << "Calling the copy constructor of Line" << endl;
len = l.len;
}
int main() {
Point myp1(1, 1), myp2(4, 5);//建立Point类的对象
Line line(myp1, myp2);//建立Line类的对象,形实结合传参数的时候,先从后面开始传
Line line2(line);//利用复制构造函数建立一个新的对象,用已经构造好的line初始化line2
cout << "The length of the line is:";
cout << line.getLen() << endl;
cout << "The length of the line2 is:";
cout << line2.getLen() << endl;
return 0;
}
12、前向引用声明
两个类互相引用,那么该如何声明? 使用前向引用声明
- 类应该先声明,后使用
- 如果需要在某个类的声明之前,引用该类,则应进行向前引用声明
- 向前引用声明只为程序引入一个标识符,但具体的声明在其他地方。
两个类互相引用的例子:
class B;//向前引用声明
class A{
public:
void f(B b);//A的成员函数f的参数是B类型的对象
};
class B{
public:
void g(A a);//B的成员函数f的参数是A类型的对象
}
前向引用声明注意事项
- 在提供一个完整的类声明之前,不能声明该类的对象,也不能在内联成员函数中使用该类对象。
- 当使用前向引用声明时,只能使用被声明的符号,而不能涉及类的任何细节。
class B;//向前引用声明
class A{
public:
B b;//错误,在提供完整类的声明之前,不能声明该类的对象,没有完整声明的都不知道该类具体细节比如说占用的大小,不能进行声明该类的对象
void f(B b);//正确,这里只是把类B的对象b当做形参,作形参只是使用了该符号,没有占用空间
};
class B{
public:
A a;//错误,在提供完整类的声明之前,不能声明该类的对象
void g(A a);//正确
}