4.类与对象

本文详细介绍了面向对象编程的基本特点,包括类和对象的概念、成员函数的定义与实现、构造函数与析构函数的作用,以及类的组合。重点讲解了类的构造函数(如默认构造函数、委托构造函数、复制构造函数)、析构函数和移动构造函数的工作原理,以及如何使用default和delete关键字。此外,还讨论了类的组合和前向引用声明在实际编程中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

4.1面向对象程序设计基本特点

4.1.1 抽象

4.2 类和对象

4.2.1 类的定义

4.2.2 类成员的访问控制

4.2.3 对象

4.2.4 类的成员函数

4.2.5 程序实例

4.3 构造函数和析构函数

4.3.1 构造函数 

4.3.2 默认构造函数

4.3.3 委托构造函数

4.3.4 复制构造函数

 4.3.5 析构函数

4.3.6 移动构造函数

4.3.7 default、delete函数

4.4 类的组合

4.4.1 组合

4.4.2 前向引用声明


4.1面向对象程序设计基本特点

4.1.1 抽象

4.2 类和对象

4.2.1 类的定义

1.时钟类定义:

class Clock {
	public:
		void setTime(int newH,int newM,int newS);
		void showtime();
	private:
		int hour,minute,second;
};
class 类名称 {
	public:
		外部接口
	protected:
	    保护型成员
	private:
	    私有成员 
}; 

注:类中可以只声明函数的原型,函数的实现(即函数体)可以在类外定义。

2.可以为数据成员提供一个类内初始值,用于初始化数据成员,没有初始值的成员将被默认初始化。

class Clock {
	public:
		void setTime(int newH,int newM,int newS);
		void showtime();
	private:
		int hour=0,minute=0,second=0;
};

4.2.2 类成员的访问控制

1.访问控制属性有三种:

公有类型(public)、私有类型(private)、保护类型(protected)

2.公有类型成员定义了类的外部接口。在类外只能访问类的公有成员。

3.私有成员只能被本类的成员函数访问,来自类外部的任何访问都是非法的。一般情况下,一个类的数据成员都应该声明为私有成员。

4.保护类成员和私有成员性质相似,其差别在于继承过程中对产生的新类影响不同。

4.2.3 对象

1.类的对象就是该类某一特定的实体。例如,将整个公司看作一个类,那么每个雇员就是该类的一个特定实体,也就是一个对象。

2.采用以下方式声明对象:

类名  对象名;

例如:Clock myClock; 就声明了一个时钟类型的对象myClock。

3.访问数据成员的一般形式是:

对象名.数据成员名

调用函数成员的一般形式是:

对象名.函数成员名(参数表)

在类的外部只能访问到类的公有成员;在类的成员函数中,可以访问到类的全部成员。

4.2.4 类的成员函数

类的成员函数描述的是类的行为

1.成员函数的实现

函数的原型声明要写在类体中,原型说明了函数的参数表和返回值类型。而函数的具体实现是写在类定义外的。具体形式为:

返回值类型 类名::函数成员名(参数表)
{
	函数体 
}

例如:

void Clock::setTime(int newH,int newM,int newS) {
	hour=newH;
	minute=newM;
	second=newS;
} 

void Clock::showTime() {
	cout<<hour<<":"<<minute<<":"<<second<<endl;
}

2.带默认形参值的成员函数

类成员函数的默认值,一定要写在类定义中,而不能写在类定义之外的函数中。例如:

class Clock {
	public:
		void setTime(int newH=0,int newM=0,int newS=0);
		...
};

3.内联成员函数

内联成员函数的函数体会在编译时被插入到每个调用它的地方。这样做可以减少调用的开销,提高执行效率,但是却增加了编译后代码的长度。所以要在权衡利弊的基础上慎重选择。

内联函数的声明有两种方式:隐式声明和显式声明。

隐式声明:

class Clock {
	public:
		void setTime(int newH,int newM,int newS);
		void showtime() {
			cout<<hour<<":"<<minute<<":"<<second<<endl;
		}
	private:
		int hour,minute,second;
};

为了保证定义的简洁,可以采用关键字inline显式声明的方式:

inline void Clock::showtime() {
	cout<<hour<<":"<<minute<<":"<<second<<endl; 
}

4.2.5 程序实例

//4_1.cpp

#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;
} 

inline void Clock::showTime() {
	cout<<hour<<":"<<minute<<":"<<second<<endl;
}

int main() {
	Clock myClock;      //定义对象myClock 
	cout<<"First time:"<<endl;
	myClock.setTime();  //设置时间为默认值 
	myClock.showTime(); //显示时间
	cout<<"Second time:"<<endl;
	myClock.setTime(11,16,8);
	myClock.showTime();
	return 0; 
} 

4.3 构造函数和析构函数

4.3.1 构造函数 

1.构造函数的作用就是在对象被创建时利用特定的值构造对象,将对象初始化为一个特定的状态。构造函数在对象被创建的时候将被自动调用。 

2.调用时无须提供参数的构造函数称为默认构造函数。如果类中没有写构造函数,编译器会自动生成一个隐含的默认构造函数。例:

//4_1_1.cpp

#include<iostream>

using namespace std;

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)
{} 

//时钟类成员函数的具体实现
void Clock::setTime(int newH,int newM,int newS) {
	hour=newH;
	minute=newM;
	second=newS;
} 

inline void Clock::showTime() {
	cout<<hour<<":"<<minute<<":"<<second<<endl;
}

int main() {
	Clock myClock(0,0,0);      //建立对象时,调用构造函数
    myClock.showTime();
	myClock.setTime(11,16,8);
	myClock.showTime();
	return 0; 
} 

由于Clock类中定义了构造函数,所以编译系统就不会再为其生成隐含的默认构造函数了。而这里自定义的构造函数带有形参,所以建立对象时就必须给出初始值,用来作为调用构造函数时的实参。

作为类的成员函数,构造函数可以直接访问类的所有数据成员,可以是内联函数,可以带有参数表,可以带默认的形参值,也可以重载。

4.3.2 默认构造函数

如果构造一个对象时不提供初始化参数,将通过一个不含参的构造函数来进行默认初始化过程,这个函数被称为默认构造函数。如果我们的类没有显式地定义构造函数,那么编译器就会隐式地定义一个默认的构造函数。

4.3.3 委托构造函数

一个委托构造函数使用他所属类的其他构造函数执行它自己的初始化过程,也就是说它把自身的一些职责委托给了其他构造函数。

Clock(int newH,int newM,int newS) { //构造函数 
	hour=newH;
	minute=newM;
	second=newS;
}

Clock():Clock(0,0,0) {}             //构造函数

第二个构造函数委托给了第一个构造函数来完成数据成员的初始化。当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体依次执行,然后控制权才会交还给委托者函数。

4.3.4 复制构造函数

1.复制构造函数是一种特殊的构造函数,具有一般构造函数的所有特性,其形参是本类的对象的引用。其作用是使用一个已经存在的对象(由复制构造函数的参数指定),去初始化同类的一个新对象。

声明和实现复制构造函数的一般方法:

class 类名 {
	public:
		类名(形参表);   //构造函数 
		类名(类名 &对象名);  //复制构造函数 
		...
};

类名::类名(类名 & 对象名){   //复制构造函数的实现 
      函数体 
} 

例:点(Point)类定义如下:

class Point {
	public:
		Point(int xx=0,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;
} 

普通构造函数是在对象创建时被调用,而复制构造函数在以下3种情况下都会被调用。

(1)当用类的一个对象去初始化该类的另一个对象时。例如:

int main() {
	Point a(1,2);
	Point b(a);       //用对象a初始化对象b,复制构造函数被调用 
	Point c=a;        //用对象a初始化对象c,复制构造函数被调用 
	cout<<b.getX()<<endl;
	return 0; 
}

(2)如果函数的形参是类的对象,调用函数时,进行形参和实参结合时。例如:

int main() {
	Point a(1,2);
	f(a);             //函数的形参为类的对象,返回函数值时,调用复制构造函数 
	return 0;
}

(3)如果函数的返回值是类的对象,函数执行完成返回调用者时。例如:

Point g() {
	Point a(1,2);
	return a;     //函数返回值是类对象,返回函数值时,调用复制构造函数 
} 

int main() {
	Point b;
	b=g();
	return 0;
}

表面上函数g将a返回给了主函数,但是a是g()的局部对象,离开建立它的函数g以后就消亡了,不可能在返回主函数后继续生存。所以在处理这种情况时编译系统会在主函数中创建一个无名临时对象,该临时对象的生存期只在函数调用所处的表达式中,也就是表达式“b=g()”中。执行语句“return a”时,实际上是调用复制构造函数将a的值复制到临时对象中。函数g运行结束时对象a消失,但临时对象会存在于表达式“b=g()”中。计算完这个表达式后,临时对象自动消失。

2.Point类的完整程序

//4_2.cpp

#include<iostream>

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;
} 

//形参为Point类对象的函数 
void fun1(Point p) {
	cout<<p.getX()<<endl;
}

//返回值为Point类对象的函数
Point fun2() {
	Point a(1,2);
	return a;
}

//主程序
int main(){
	Point a(4,5);   //第一个对象a 
	Point b=a;      //情况一,用a初始化b。第一次调用复制构造函数。 
	cout<<b.getX()<<endl;
	fun1(b);        //情况二,对象b作为fun1的实参 
	b=fun2();       //情况三,函数的返回值是类对象 
	cout<<b.getX()<<endl;
	return 0; 
}

运行结果为:

Calling the copy constructor
4
Calling the copy constructor
4
1

 4.3.5 析构函数

1.简单来说,析构函数与构造函数的作用几乎正好相反,它用来完成对象被删除前的一些清理工作析构函数是在对象的生存期即将结束的时刻被自动调用的。它的调用完成后,对象也就消失了,相应的内存空间也被释放。

析构函数不接受任何参数。一般来讲,如果希望程序在对象被删除之前的时刻自动完成某些事情,就可以把它们写到析构函数中。

4.3.6 移动构造函数

1.左值绝大多数时候在等号左边,右值在右边,但这并不总是对的。

例如:int i=10 ;变量i是一个在内存中有位置的实际变量,数字10没有存储空间,没有位置,所以我们就不能写成int 10=i,因为10没有位置,我们不能在10中存储数据。但是,我们可以让另一个变量int a=i;这样一个左值就等于了另一个左值。

2.右值不仅只是字面量,也可以是函数结果。

#include<iostream>

int GetValue() {
	return 10;
}

int main() {
	int i=GetValue(); //正确 
	GetValue()=5;     //编译错误,因为GetValue()是一个右值 
}

要解决上面的问题,可以让函数返回值是左值(即左值引用)

#include<iostream>

int& GetValue() {  //左值引用 
	static int value=10;
	return value;
}

int main() {
	int i=GetValue(); //正确 
	GetValue()=5;     //正确
}

3.展开来说,如果一个函数,它有一个值,我们可以用左值或右值来调用它。

#include<iostream>

void SetValue(int value) {
	
} 

int main() {
	int i=10;
	SetValue(i);
	SetValue(10);    //临时值 
}
#include<iostream>

void SetValue(int& value) {
	
} 

int main() {
	int i=10;
	SetValue(i);
	SetValue(10);    //错误,不能用左值来引用右值 
}

但是如果改用const引用,则可以解决上述问题。const左值引用可以同时接受两个值。

void SetValue(const int& value) {
	
} 

int main() {
	int i=10;
	SetValue(i);     //正确 
	SetValue(10);    //正确 
}

4.右值引用不能传递左值,但可以传递右值

void PrintName(std::string&& name) {
	std::cout<<name<<std::endl
} 

int main() {
	std::string firstname="wang";
	std::string lastname="wenhao";
	
	std::string fullname=firstname+lastname;
	
	PrintName(fullname);       //错误 ,右值引用不能传递左值 
	PrintName(firstname+lastname);
}

5.特殊情况

#include<iostream>
void PrintName(const std::string& name) {
	std::cout<<"[lvalue]:"<<name<<std::endl; 
} 

void PrintName(std::string&& name) {
	std::cout<<"[rvalue]:"<<name<<std::endl;
} 


int main() {
	std::string firstname="wang";
	std::string lastname="wenhao";
	
	std::string fullname=firstname+lastname;
	
	PrintName(fullname);       
	PrintName(firstname+lastname);
}

运行结果:

[lvalue]:wangwenhao
[rvalue]:wangwenhao

4.3.7 default、delete函数

1.C++11标准提供了default和delete两个关键字来简化构造函数的定义与使用。使用=default可显示要求编译器自动生成默认或复制构造函数。

通过使用default,可以让编译器合成简单的无参默认构造函数和复制构造函数,但其它使用参数的构造函数,需要用户自己定义。当用户不希望定义的类存在复制时,可以通过delete关键字将复制构造函数删除。

与default使用不同的是,delete不限于在无参和复制构造函数上使用,除析构函数外,用户都可以指定为delete删除掉。

4.4 类的组合

4.4.1 组合

1.类的组合描述的就是一个类内嵌其它类的对象作为成员的情况,他们之间的关系是一种包含与被包含的关系。

当创建类的对象时,如果这个类具有内嵌对象成员,那么各个内嵌对象将被自动创建。在创建对象时既要对本类的基本类型数据成员进行初始化,又要对内嵌对象成员进行初始化。

2.组合类构造函数的一般定义为:

类名::类名(形参表),内嵌对象1(形参表),内嵌对象2(形参表),...
{类的初始化}

Circle::Circle(float r):radius(r) {}

创建一个组合类的对象时,构造函数的调用顺序如下:

(1)调用内嵌对象的构造函数,调用顺序按照内嵌对象在组合类的定义中出现的次序。

(2)执行本类构造函数的函数体。

析构函数的调用执行顺序与构造函数刚好相反。

3.要为组合类编写复制构造函数,则需要为内嵌成员对象的复制构造函数传递参数。

C::C(C &c1):b(c1.b) {...} 

4.类的组合,线段(Line)类

//4_4.cpp
#include<iostream>
#include<cmath>

using namespace std;

class Point {  //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 getLine() {return len;}
	private:
		Point p1,p2;
		double len;
}; 

//组合类的构造函数
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() {
	Point myp1(1,1),myp2(4,5);
	Line line(myp1,myp2);
	Line line2(line);;
	cout<<"len:"<<line.getLine()<<endl;
	cout<<"len2:"<<line2.getLine();
	return 0;
}

在整个程序运行过程中,Point类的复制构造函数被调用了6次,而且都是在Line类构造函数体运行之前进行的,分别为:两个对象在Line构造函数进行函数参数形实结合时,初始化内嵌对象时,以及复制构造line2时被调用的。

4.4.2 前向引用声明

1.先看一个例子:

class A{              //A类的定义 
	public:           //外部接口 
		void f(B b);  //以B类对象b为形参的函数成员 
};

class B{              //B类的定义 
	public:           //外部接口
		void g(A a);  //以A类对象a为形参的函数成员 
};

由于在使用一个类之前,必须首先定义该类,因此无论将哪一个类的定义放在前面,都会引起编译错误。解决这种问题的办法,就是使用前向引用声明。

class B;              //前向引用声明 
class A{              //A类的定义 
	public:           //外部接口 
		void f(B b);  //以B类对象b为形参的函数成员 
};

class B{              //B类的定义 
	public:           //外部接口
		void g(A a);  //以A类对象a为形参的函数成员 
};

 注意,尽管使用了前向引用声明,但是在提供一个完整的类定义之前,不能定义该类的对象,也不能在内联成员函数中使用该类的对象。

class Fred;            //前向引用声明
class Barney {
		Fred x;                //错误:类Fred的定义尚不完善 
};

class Fred {
		Barney y;
};

再看下面一段程序:

class Fred;                    //前向引用声明
class Barney {
	public:
		...
		void method() {
			x.yabbaDabbaDo();  //错误:Fred类的对象在定义之前被使用
		}
	private:
		Fred &x;                   //正确:经过前向引用声明,可以声明Fred类的对象引用或指针
};

class Fred {
	public:
		...
		void yabbaDabbaDo();
	private:
		Barney &y
};

编译器编译时会指出错误,因为在类Barney的内联函数中使用了由x所指向的,Fred类的对象,而此时Fred类尚未被完整地定义。解决方法是,更改这两个类的定义次序,或者将函数method()改为非内联形式,并且在Fred的完整定以之后,再给出函数的定义。

注意:当使用前向引用声明时,只能使用被声明的符号,而不能涉及类的任何细节。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值