类与对象
抽象:是对同一类对象的共同属性和行为进行概括,形成类。数据抽象:描述某类对象的属性或状态(对象相互区别的物理量)- 代码抽象:描述某类对象的共有的行为特征或具有的功能
- 抽象的实现:类
封装:将抽象出的数据,代码封装在一起,形成类。目的,使用者不必了解具体的实现细节,而只需要通过外部接口,以特定的访问权限,来使用类的成员- 实现封装:类声明中的{}
类与对象的关系
对象是现实中的对象在程序中的模拟- 类是同一类对象的抽象,对象是类的
某一特定实体 - 定义类的对象,才可以通过对象使用
类中定义的功能。
class 类名称
{
public: //任何外部函数都可以访问共有类型数据和函数
公有成员(外部接口)
private: //只允许本类中额函数访问,类外部函数不能访问
私有成员
protected: //为有继承关系设计
保护型成员
}对象定义的语法:
Clock myClock;
//类中成员相互访问:直接使用成员名访问
//类外
对象名.成员名
//访问public属性的成员类内初始化
- 可以为
数据成员提供一个类内初始值 - 在创建对象时,类内初始值用于初始化数据成员
- 没有初始值的成员将被默认初始化
类的成员函数
- 在类中说明
函数原型 - 可以在
类外给出函数体实现,并在函数名前使用类名加以限定 - 也可以直接在类中给出函数体,形成内联成员函数
- 允许声明重载函数和带默认参数值的函数
内联成员函数
- 为了提高运行时的
效率,对于较简单的函数可以声明为内联形式 - 内联函数体中
不要有复杂结构(如循环语句和switch语句) - 在类中声明内联成员函数的方式
-
- 将函数体放在类的生命中
- 使用
inline关键字
/*
* project:钟表类
* environment :VS2022
* time:2023年1月31日
*/
#include <iostream>
using namespace std;
class Clock //定义类边界
{
public: //特定访问权限
//外部接口,并赋默认参数值
void setTime(int newH = 0, int newM = 0, int newS = 0);
void showTime();
private: //公有函数共享私有成员
int hour, minute, second;
};
//成员函数的实现,类中的函数
void Clock::setTime(int newH, int newM, int newS)
{
hour = newH;
minute = newM;
second = newS;
}
void Clock::showTime()
{
cout << "Time:" << hour << ":" << minute << ":" << second << endl;
};
int main(void)
{
Clock C; //定义对象
cout << "First time set and output:" << endl;
C.setTime(); //类外访问,使用默认时间
C.showTime(); //x显示时间
cout << "Second time set and output:" << endl;
C.setTime(12, 48, 30); //类外访问
C.showTime();
return 0;
}构造函数
构造函数:在对象被创建时使用特定的值构造对象,将对象
初始化为一个特定的初始状态。
构造函数形式:
- 函数名与类名相同
- 不能定义返回值类型,也不能有return语句
- 可以有形式参数,也可以没有形式参数
- 可以是内联函数
- 可以重载
- 可以带默认参数值
构造函数的调用时机
在对象创建时被自动调用。
隐含生成的构造函数
- 如果程序中未定义构造函数,编译器将在需要时自动生成一个默认构造函数(并不是全部)
- 参数列表为空,不为数据成员设置初始值
- 如果类内定义了成员的初始值,则使用类内定义的初始值
- 如果没有定义类内的初始值,则以默认方式初始化
- 基本类型的数据默认初始化的值是不确定的
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)//描述初始化要求,不用再写,初始化直接装进去
{}=default
如果类中已定义构造函数,默认情况下编译器就不再隐含生成默认构造函数。如果此时依然希望编译器隐含生成默认构造函数,可以使用
=default
class Clock //定义类边界
{
public: //特定访问权限
Clock() = default; //生成默认构造函数
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():Clock(0,0,0){}复制构造函数
- 我们经常会需要用一个
已经存在的对象,去初始化新的对象–复制构造函数 - 隐含生成的复制构造函数可以实现对应数据成员一一复制
- 自定义的复制构造函数可以实现特殊的复制功能
复制构造函数是一种特殊的构造函数,其形参为本类的对象引用。作用是用一个已存在的对象去初始化同类型的新对象.
class 类名
{
public:
类名(形参); //构造函数
类名(const 类名 & 对象名); //复制构造函数,const常引用
//如果不希望对象被复制构造
类名(const 类名 & 对象名) = delete; //指示编译器不生成默认复制构造函数
//...
}
类名::类 (const 类名 & 对象名) //复制构造函数的实现
{
函数体
}复制构造函数被调用的三种情况
- 定义一个对象时,以本类另一个对象作为
初始值,发生复制构造, - 如果函数的形参是类的对象,调用函数时,将使用实参对象初始化形参对象,发生复制构造
- 如果函数的返回值是类的对象,函数执行完成返回主调函数时,将使用return语句中的对象初始化一个临时无名对象,传递给主调函数,此时发生复制构造。(一般此项编译器可以选择取消)。
/*
* project:复制构造函数
* environment :VS2022
* time:2023年1月31日
*/
#include <iostream>
using namespace std;
class Point
{
public:
Point(int xx = 0, int yy = 0)
{
x = xx;
y = yy;
}; //构造函数
Point(const Point& p);//复制构造函数
void setX(int xx)
{
x = xx;
}
void setY(int yy)
{
y = yy;
}
int getX() const { return x; } //常函数
int getY() const { return y; }
private:
int x,y;
};
//复制构造函数的实现
Point::Point(const 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(1, 2);
return a;
}
int main(void)
{
Point a(4,5);
Point b(a); //用a初始化b
cout << b.getX() << endl;
fun1(b); //对象b作为fun1的实参
b = fun2(); //函数的返回值是类对象
cout << b.getX() << endl;
return 0;
}左值右值
左值位于赋值
运算左侧的对象或变量,右值是位于赋值运算右侧的值。
- 对
持久存在变量的引用称为左值引用,用&表示 - 对
短暂存在可被移动的右值的引用称之为右值引用,用&&表示
float n = 6;
float &lr_n = n; #左值引用
float &&rr_n = n; #错误,右值引用不能绑定到左值
float &&rr_n = n*n; #右值表达式绑定到右值引用通过标准库<utility>中的move函数可将左值对象强行移动为右值
float n = 10;
float &&rr_n = std::move(n); //将n转化为右值
//把n的值转给rr_n了,从此n就不能用了移动构造函数
基于右值引用,移动构造函数通过移动数据方式构造新对象,与复制构造函数类似,移动构造函数参数为该类对象的右值引用
#include <utility>
class astring
{
public:
std::string s;
astring (astring&& o) noexcept:s(std::move(o.s)) //显式移动所有成员
{函数体}
}-
移动构造函数
不分配新内存,理论上不会报错,为配合异常捕获机制,需声明noexcept表明不会抛出异常 -
被移动的对象不应再使用,需要
销毁或重新赋值
析构函数
- 完成对象
被删除前的一些清理工作 - 在对象的生存期结束的时刻系统自动调用它,然后再释放此对象所属的空间
- 如果程序中未声明析构函数,
编译器将自动产生一个默认的析构函数,其函数体为空。
函数名前加 ~类的组合
- 类中的
成员是另一个类的对象 - 可以在已有抽象的基础上实现更复杂的抽象
原则:不仅要负责对本类中的基本类型成员数据初始化,也要对对象成员初始化
类名::类名(对象成员所需的形参,本类成员形参)
:对象1(参数),对象2(参数),.....
{
函数题其他语句
}构造组合类对象时的初始化次序
-
首先对构造函数初始化列表中列出的成员(包括
基本类型成员和对象成员)进行初始化,初始化次序是成员在类体中定义的次序- 成员对象构造函数调用顺序:按对象成员的生命顺序,
先声明先构造 - 初始化列表中未出现的成员对象,调用默认构造函数(即无形参)初始化
- 成员对象构造函数调用顺序:按对象成员的生命顺序,
-
处理完初始化列表之后,再执行构造函数的函数体
/*
* project:类的组合,线段(Line)类
* environment :VS2022
* time:2023年1月31日
*/
#include <iostream>
#include <cmath>
using namespace std;
class Point
{
public:
Point(int xx = 0, int yy = 0)
{
x = xx;
y = yy;
}; //构造函数
Point(const Point& p);//复制构造函数
int getX() const { return x; } //常函数
int getY() const { return y; }
private:
int x, y;
};
//类的组合
class Line
{
public: //外部接口
Line(Point xp1, Point xp2);
Line(Line& l);
double getLen()
{
return len;
}
private: //私有数据成员
Point p1, p2; //Point类的对象P1,P2
double len;
};
Point::Point(const Point& p)
{
x = p.x;
y = p.y;
cout << "Calling the copy constructor" << endl;
}
//组合类的构造函数
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); //如果长度断点可以变化
}
Line::Line(Line& l) :p1(l.p1), p2(l.p2) //组合类的复制构造函数
{
cout << "Calling the copy constructor of Line" << endl;
len = l.len;
}
int main(void)
{
Point myp1(1, 1), myp2(4, 5); //建立Point 类的对象
Line line(myp1, myp2); //建立Line类的对象
Line line2(line); //利用复制构造函数建立一个新对象
cout << "The length of line is";
cout << line.getLen() << endl;
cout << "The length of the line2 is:" << line2.getLen() << endl;
return 0;
}前向引用声明
- 类应该先
声明,后使用 - 如果需要在某个类的生命之前,引用该类,则应进行前向引用声明
- 前向引用声明只为程序引入一个
标识符,但具体声明在其他地方 - 在提供一个完整的类声明之前,不能声明该类的对象,也不能在内联成员函数中使用该类的对象
- 当使用前向引用声明时,只能使用被
声明的符号,而不能涉及类的任何细节。
class B; //前向引用声明,不引入细节,
class A
{
public:
void f(B b);
};
class B
{
public:
void g(A a);
};参考资料:
- 郑莉老师《C++语言程序设计》
