C++学习笔记:类和对象

目录

2.1类的定义

2.1.1成员访问权限

2.1.2成员函数实现方式

2.1.3成员函数内联(inline)

2.1.4成员函数重载

2.2对象的创建和使用

2.2.1对象访问其成员

2.2.2对象的指针访问其成员

2.2.3对象的引用访问其成员

2.3构造函数

2.3.1默认构造函数

1.系统自动生成默认构造函数

2.自定义默认构造函数

(1)定义无参的构造函数

(2)参数有默认值的构造函数

2.3.2构造函数的重载

2.3.3类定义时成员变量初始化

2.3.4构造函数与初始化列表

2.4析构函数

2.5构造和析构的调用顺序

2.5.1全局变量

2.5.2局部变量

2.5.3动态内存

2.5.4其他情况

2.6对象数组

2.7对象指针

2.8this指针

2.9向函数传递对象

2.9.1对象作为函数参数

2.9.2对象指针作为函数参数

2.9.3对象引用作为函数参数

2.9.4三种传递方式比较

2.10对象的拷贝构造和赋值

2.10.1拷贝构造函数

2.10.2赋值

2.11组合类


2.1类的定义

类的定义格式如下:

class 类名
{
private:
    成员属性或成员函数
protected:
    成员属性或成员函数
public:
    成员属性或成员函数
};

说明:

(1)class是声明类的关键字,class后跟类名。类名一般首字母大写;

(2)类包括成员属性和成员函数。成员属性代表对象的属性;成员函数实现对象的行为;

(3)private、protected和public关键字称为访问权限符,它规定了类中成员的访问属性。这三个关键字可以按任意顺序出现。默认时为私有的(private)。

例如:声明一个学生类。

分析:每个学生都有学号、姓名和性别;对于学生的基本操作有输入信息、输出信息等。因此,对应的学生类定义如下:

class Student //声明类
{
private://访问权限:私有成员
    string m_id;    //属性,数据成员,表示学号
    string m_name;  //属性,数据成员,表示姓名
    string m_sex;   //属性,数据成员,表示性别
public: //访问权限:公有成员
    Student();      //行为,成员函数的原型声明,表示构造函数  
    void input();   //行为,成员函数的原型声明,表示输入学生信息
    void print();   //行为,成员函数的原型声明,表示输出学生信息
};//类声明结束

2.1.1成员访问权限

可以定义3中不同的访问权限符,分别为public(公有类型)、private(私有类型)和protected(保护类型)

public(公有类型)

        public声明成员为公有成员。完全公开。都可以访问。

class Human 
{
public://声明类的公有成员
	int high;//身高
	int weight;//体重
	void GetHigh()
	{
		cout << "身高:" << high << endl;
	}
	void GetWeight()
	{
		cout << "体重:" << high << endl;
	}
};

int main()
{
	Human zhangsan;        //定义类的对象
    zhangsan.high=175;     //通过对象访问类的公有数据成员
    zhangsan.weight=70;    //通过对象访问类的公有数据成员
    zhangsan.GetHigh();    //通过对象访问类的公有成员函数
    zhangsan.GetWeight();  //通过对象访问类的公有成员函数

	return 0;
}

private(私有类型)

private声明成员为私有成员。该级别的成员只能被它所在类中的成员函数和该类的友元函数访问。

class Human //不严谨
{
private://声明类的私有数据成员
	int high;//身高
	int weight;//体重
public://声明类的公有成员函数
	//int high;//身高
	//int weight;//体重
	//Human()//系统自动调用,进行初始化
	//{
	//	high = 170;
	//	weight = 70;
	//}

	void SetHigh(int h)
	{
		high = h;//类的成员函数访问类的私有数据成员
	}
	void GetHigh()
	{
		cout << "身高:" << high << endl;//类的成员函数访问类的私有数据成员
	}
	void SetWeight(int w)
	{
		weight = w;//类的成员函数访问类的私有数据
	}
	void GetWeight()
	{
		cout << "体重:" << high << endl;//类的成员函数访问类的私有
	}
};

int main()
{
	Human list;//定义类的对象
	//list.high = 185;//错误,不能通过对象访问类的私有数据成员
	//list.weight = 90;//错误,不能通过对象访问类的私有数据成员
	list.SetHigh(180);    //通过对象访问类的公有成员函数给high赋值
	list.SetWeight(80);   //通过对象访问类的公有成员函数给weight赋值
	list.GetHigh();		  //通过对象访问类的公有成员函数
	list.GetWeight();	  //通过对象访问类的公有成员函数

	return 0;
}

从上例可知,private成员只能在类的成员函数中使用。在类外,不能通过对象访问。

protected(保护类型)

protected声明成员为保护成员。具有这个访问控制级别的成员,外界是无法直接访问的。它只能被它所在类及从该类派生的子类的成员函数及友元函数访问。它和private的区别只在类继承时体现。

总结public、protected和private这三种访问限定符对应的类成员的可访问性如下表:

访问限定符类成员函数子类成员函数友元其他函数
public可以可以可以可以
protected可以可以可以不可以
private可以不可以可以不可以

2.1.2成员函数实现方式

类的成员函数也是函数的一种。它与一般函数的区别是:它属于一个特定的类,是类的一个成员。

在使用类的成员函数时,要注意它的访问权限(它能否被访问),以及它的作用域(类函数能在什么范围内被访问)。

类的成员函数的定义方式有两种。

        第一种是在类内直接进行定义。这种方式一般用在代码比较少的成员函数中,并自动生成内联函数。

        第二种是在类中进行函数说明,在类外进行函数定义。这种情况通常用在代码较多的类的成员函数上。在定义函数时,必须用作用域“::”表明函数所属的类。

形式如下:

返回类型 类名::函数名(参数列表)
{
    //函数体
}

例如:定义时钟类

class Clock
{
private:
	int hour;//小时
	int minute;//分钟
	int second;//秒
public:
	void setTime(int h, int m, int s);//类中声明,类外定义

	void setTime(int h, int m)
	{
		hour = h;
		minute = m;
		second = 0;
	}

	void showTime()//类中定义函数
	{
		cout << hour << "时" << minute << "分" << second << "秒" << endl;
	}
};

void Clock::setTime(int h, int m, int s)//定义成员函数
{
	hour = h;
	minute = m;
	second = s;
}

int main()
{
	Clock cc;
	cc.setTime(8, 10, 20);
	cc.showTime();
	return 0;
}

2.1.3成员函数内联(inline)

        内联函数是C++为提高程序运行速度所做的一项改进。常规函数和内联函数之间的主要区别不在于编写方式,而在于C++编译器如何将它们组合到程序中。

        当程序执行函数调用时,系统要为即将调用的函数创建栈空间(栈帧),保存现在正在执行的函数数据(保护现场),传递参数以及控制程序执行的跳转等操作,当调用的函数执行完成后又需要跳到当前函数并恢复现在的数据(恢复现场),然后继续执行,这些操作都需要时间和空间开销。

        C++内联(inline)函数提供了另一种选择,对于内联函数编译器将用函数代码替换函数调用,这样程序就无需跳到另一个函数执行,执行完后再跳回来。因此内联函数的运行速度比常规函数稍快。

例如:求两个数的最大值

inline int max(int x, int y)
{
	return x > y ? x : y;
}

int main()
{
	int z1, z2, z3;
	z1 = max(10, 20);//调用函数,在编译时进行代码替换
	z2 = max(20, 50);//调用函数,在编译时进行代码替换
	z3 = max(z1, z2);//调用函数,在编译时进行代码替换
	cout << "z1=" << z1 << endl;
	cout << "z2=" << z2 << endl;
	cout << "z3=" << z3 << endl;
	return 0;
}

上面程序中,main()函数3次调用了内联函数max(),在编译过程中展开类似下面的形式:

int z1 = 10 > 20 ? 10 : 20;
int z2 = 20 > 50 ? 20 : 50;
int z3 = z1 > z2 ? z1 : z2;

        程序员请求将函数作为内联函数时,编译器不一定满足这种要求。内联inline只是对编译器提出建议,而有的编译器没有启用或者实现这种特性。一般内联函数应该满足下面的要求:

(1)函数不能出现循环;

(2)函数代码不超过三行。

类中的内联函数

        内联函数主要的应用场景就是类。如果类的成员函数非常简单(不超过三行代码),可以将成员函数定义在类中,这时该函数自动成为内联函数,如果成员函数较复杂(超过三行),则在类中声明成员函数,而在类外定义。

内联和宏

        inline是C++新增的特性。C语言使用预处理语句#define来实现宏。宏最常用的功能是让数字有自己的名字。而不是让它做函数该做的事情。下面是一个计算平方的宏:

#define SQUARE(x) x*x

宏仅仅是字符替换。下面的语句执行结果可能和你想的并不一样。

int a = 1;
int b = SQUARE(2+3);//被替换成 int b=2+3*2+3;
int c = SQUARE(++a);//被替换成 int c=++a*++a;这个a被++两次

这里的目的不是演示如何编写宏,而是要指出,C语言中的宏仅仅只是字符替换,如果要让宏执行类似函数的功能,请考虑把它改为内联函数。

2.1.4成员函数重载

        成员函数重载是指在同一个类里,有两个以上的函数具有相同的函数名。每种实现对应着一个函数体,但是形参的个数或者类型不同。

        例如:减法函数重载

        创建一个类,在类中定义3个名为subtract的重载成员函数,分别实现两个整数相减、两个实数相减和两个复数相减的功能。

struct Complex//复数类型
{
	double real;//实部
	double imag;//虚部
};

class Sub//减法重载类
{
public:
	int subtract(int x, int y);              //两个整数相减
	double subtract(double x, double y);     //两个double相减
	Complex subtract(const Complex& x, const Complex& y);//两个复数相减
};

int Sub::subtract(int x, int y)
{
	return x - y;
}

double Sub::subtract(double x, double y)
{
	return x - y;
}

Complex Sub::subtract(const Complex& x, const Complex& y)
{
	Complex c;
	c.real = x.real - y.real;//实部相减
	c.imag = x.imag - y.imag;//虚部相减
	return c;
}

int main()
{
	int m = 30;
	int n = 20;
	double x = 34.5;
	double y = 23.2;
	Complex a = { 10,10 };
	Complex b = { 5,5 };
	Sub s;
	cout << m << "-" << n << "=" << s.subtract(m, n) << endl;
	cout << x << "-" << y << "=" << s.subtract(x, y) << endl;
	Complex c = s.subtract(a, b);
	cout << "(" << a.real << "+" << a.imag << "i)" << "-" << b.real << "+" << b.imag << "i)" << "=" << "(" << c.real << "+" << c.imag << "i)" << endl;
	return 0;
}

程序运行结果如下:

2.2对象的创建和使用

        定义了类,就相当于定义了一个数据类型。类与int、char等数据类型的使用方法是一样的。可以是定义变量、数组和指针等。使用类定义的变量通常称为该类的对象

        对象的定义格式如下:

        类名 对象名;

2.2.1对象访问其成员

对象通过"."访问它的成员变量和成员函数。

对象名.成员变量 //访问对象的成员变量
对象名.成员函数 //访问对象的成员函数

例如下面的代码,创建一个日期类

class Date  //日期类
{
private:
	int year;//年
	int month;//月
	int day;//日
public:
	void set(int y, int m, int d)//赋值操作,在类声明中定义
	{
		year = y;
		month = m;
		day = d;
	}
	bool isLeapYear()//判断闰年,在类声明中定义
	{
		return year % 4 == 0 && year % 100 != 0 || year % 400 == 0;
	}
	void show();//输出日期
};

void Date::show()//输出日期,在类声明外定义
{
	cout << year << "年" << month << "月" << day << "日" << endl;
}

int main()
{
	Date d;                 //创建一个Data的对象,名字为today
	d.set(2023, 4, 21);     //today访问它的set函数
	d.show();               //today访问它的show函数
	if (d.isLeapYear())     //today访问它的isLeapYear函数
		cout << "今年是闰年" << endl;
	else
		cout << "今年不是闰年" << endl;

	return 0;
}

上述代码中,创建了类的对象d之后,系统就要为对象分配内存空间,用于储存对象成员。

2.2.2对象的指针访问其成员

对象的指针通过“->”访问它的成员变量和成员函数。

对象的指针->成员变量
对象的指针->成员函数

如下代码:

int main()
{
	Date d;
	d.set(2023, 4, 21);
	Date* p = &d;
	p->show();//对象指针通过->访问它的成员

	return 0;
}

2.2.3对象的引用访问其成员

        对象定义一个引用变量,它们是共占用一段存储单元的,实际上它们是同一个对象,只是用不同的名字表示而已。因此完全可以通过引用变量来访问对象中的成员。

        对象定义引用的方式:

类型 &引用变量名=对象名;

例如:通过对象的引用变量来访问对象的数据成员和成员函数。

class Time
{
public:
	int hour;
	int minute;
	int second;
	void showTime();
};

void Time::showTime()
{
	cout << hour << "时" << minute << "分" << second << "秒" << endl;
}

int main()
{
	Time t1 = { 16,48,56 };//创建Time对象ti
	Time& t2 = t1;        //t2是t1的引用
	t1.showTime();        //通过t1访问其成员函数
	t2.showTime();        //通过引用变量访问对象的成员函数
	return 0;
}

程序运行结果如下:

2.3构造函数

构造函数是一种特殊的成员函数,专门用于构造新对象,并把数据赋值给它的成员。

构造函数在类内的定义格式如下:

类名 (参数列表)
{
    函数体;
}

构造函数可以在类内也可以在类外定义。在类外定义构造函数的形式如下:

类名::类名(形参列表)
{
    函数体;
}

说明:

(1)构造函数的名称必须与类名相同;

(2)构造函数没有返回值类型,也不能指定为void;

(3)构造函数可以重载,即可以有多个构造函数;

(4)如果没有显式定义构造函数,系统会自动生成一个默认的构造函数。这个构造函数不含参数,也不对数据成员进行初始化,只负责为对象分配存储空间。

(5)如果显式地为类定义了构造函数,系统将不再为类提供默认构造函数。

(6)定义对象时,系统会自动调用构造函数。

(7)构造函数一般被定义为公有访问权限。

例如:构造函数的使用

class Date
{
private:
	int year;
	int month;
	int day;
public:
	Date(int y, int m, int d);	//构造函数
	Date(int y)
	{
		year = y;
		month = 1;
		day = 1;
	}
	void Output();//输出函数
};

Date::Date(int y, int m, int d)//定义构造函数
{
	year = y;
	month = m;
	day = d;
}

void Date::Output()//定义成员函数
{
	cout << year << "-" << month << "-" << day << endl;
}

int main()
{
	Date today(2023, 4, 21);
	today.Output();

	return 0;
}

2.3.1默认构造函数

        不需要提供参数的构造函数,可以由系统提供(不写任何构造函数)或程序提供(定义无参构造函数,或所有参数都有默认值的构造函数)

1.系统自动生成默认构造函数

        如果你没有编写任何构造函数则系统会自动生成默认构造函数。它只负责为对象分配储存空间,而不对数据进行初始化。

class Point //点类
{
private:
	int x;//x坐标
	int y;//y坐标
public:
	void show()//输出函数
	{
		cout << "x=" << x << ",y=" << y << endl;
	}
};

int main()
{
	Point p1;
	p1.show();//输出的值没有初始化过,千万不要这样使用

	return 0;
}

程序结构如下:

注意:系统虽然可以生成默认构造函数,但是它并不是给我们使用的,我们一定要定义自己的构造函数。

2.自定义默认构造函数

(1)定义无参的构造函数
class Point //点类
{
private:
	int x;//x坐标
	int y;//y坐标
public:
	Point()//默认构造函数,也是无参构造函数
	{
		cout << "自定义默认构造函数" << endl;
		x = 0;//自己定义的就可以对成员变量进行初始化
		y = 0;
	}
	void show()//输出函数
	{
		cout << "x=" << x << ",y=" << y << endl;
	}
};

int main()
{
	Point p2;
	p2.show();//输出的值没有初始化过,千万不要这样使用

	return 0;
}

(2)参数有默认值的构造函数
class Point //点类
{
private:
	int x;//x坐标
	int y;//y坐标
public:
	Point(int m=10,int n=10)//所有参数都有默认值的构造函数
	{
		cout << "自定义构造函数,所有参数都有默认值" << endl;
		x = m;
		y = n;
	}
	void show()//输出函数
	{
		cout << "x=" << x << ",y=" << y << endl;
	}
};

int main()
{
	Point p1;//使用默认的值
	p1.show();

	Point p2 = { 100,200 };//传入实参
	p2.show();

	return 0;
}

程序执行结果如下:

说明:

(1)默认参数只能最上面给,不能多处给定(避免不一致)

(2)带默认值的参数必须在最右面;

(3)有默认参数时,注意避免重定义。

上面三点和普通带默认值的函数一样,在这仅仅是提醒。

2.3.2构造函数的重载

一个类可以定义多个构造函数,以便为类的对象提供不同的初始化方法,供用户选择使用。这些构造函数具有相同的名字,但参数列表不同。

例如:构造函数重载。

class Date
{
private:
	int year;
	int month;
	int day;
public:
	Date();						//无参的构造函数
	Date(int y, int m, int d);  //有参的构造函数
	void show();
};

Date::Date()
{
	year = 2024;
	month = 4;
	day = 21;
}
Date::Date(int y, int m, int d)
{
	year = y;
	month = m;
	day = d;
}

void Date::show()
{
	cout << year << "-" << month << "-" << day << endl;
}

int main()
{
	Date today;
	Date tomorrow = { 2024,4,22 };
	today.show();
	tomorrow.show();

	return 0;
}

程序执行结果如下:

2.3.3类定义时成员变量初始化

在C++11中允许在类定义时对成员变量初始化。

class A
{
public:
	A()
	{
		m_a = 10; m_b = 0;
	}
	void show()
	{
		cout << "m_a=" << m_a << endl;
		cout << "m_b=" << m_b << endl;
	}
private:
	int m_a = 10;
	int m_b;
};

int main()
{
	A a;
	a.show();

	return 0;
}

如果在构造函数中也有赋值,以赋值的为准,这个就和普通变量一样,初始化的值会被后面的赋值覆盖。

class A
{
public:
	A(int b = 0) :m_b(b)
	{}
	A(int a, int b) :m_a(a), m_b(b)
	{}
	void show()
	{
		cout << "m_a=" << m_a << endl;
		cout << "m_b=" << m_b << endl;
	}
private:
	int m_a = 10;//类定义时初始化
	int m_b;	 //没有初始化
};

int main()
{
	A a;
	cout << "a的数据:" << endl;
	a.show();

	A b{ 100 };
	cout << "b的数据:" << endl;
	b.show();

	A c{ 1000,2000 };
	cout << "c的数据:" << endl;
	c.show();

	return 0;
}

说明:

(1)第20行,A a;调用第4行的构造函数,m_a是第10行的初始值10,m_b是b的默认参数0;

(2)第24行,A b{100};调用第4行构造函数,m_a是第10行的初始值10,m_b是传入的实参100;

(3)第28行,A c{1000,2000};调用第6行构造函数,m_a是传入的实参1000,m_b是传入的实参2000。

2.3.4构造函数与初始化列表

构造函数也可以采用构造初始化列表的方式对数据成员进行初始化。

例如,可以把上面的构造函数的定义

Date::Date(int y, int m, int d)
{
	year = y;
	month = m;
	day = d;
}

改写为:

Date::Date(int y, int m, int d) :year(y),month(m),day(d)
{}

利用构造函数定义(声明)对象的其它方式

class Date
{
private:
	int year;
	int month;
	int day;
public:
	Date(int y = 1970, int m = 1, int d = 1);  //有参的构造函数
	void show();
};

Date::Date(int y, int m, int d) :year(y),month(m),day(d)
{}
void Date::show()
{
	cout << year << "-" << month << "-" << day << endl;
}

int main()
{
	Date d1(2023, 4, 21);//定义对象d1
	Date d2;//定义对象d2,利用默认构造函数
	//Date d3();//注意不能这样写,这是函数声明
	d1.show();
	d2.show();
	Date d4 = { 2040,1,1 };//定义对象d4,列表初始化,类似结构体 C++11
	d4.show();
	Date d5{ 2050,2,2 };//定义对象d4,列表初始化 C++11
	d5.show();
	Date d6 = Date{ 2060,3,3 };//定义对象d6,传统写法
	d6.show();
	Date* d7 = new Date(2070, 4, 4);//定义对象d7,动态创建
	d7->show();

	return 0;
}

2.4析构函数

当对象的生存期结束时,系统就会自动执行析构函数清除其数据成员所分配的内存空间。

析构函数的定义格式为:

~类名();//没有返回值,没有参数

说明:

(1)析构函数名是由"~"加类名组成的;

(2)析构函数没有参数、没有返回值,不能重载;

(3)一个类有且仅有一个析构函数,必须为public;

(4)在对象的生存期结束时,由系统自动调用析构函数。

(5)如果没有定义析构函数,系统会自动生成一个默认的析构函数,这个析构函数不做任何事情。

例如:析构函数应用

class Student
{
private:
	string name;
	int number;
public:
	Student(string na, int nu);//构造函数
	~Student();//析构函数
	void show();//输出函数
};

Student::Student(string na, int nu)//构造函数定义
{
	cout << "构造中……" << endl;
	name = na;
	number = nu;
}

Student::~Student()//析构函数定义
{
	cout << "析构中……" << endl;
}
void Student::show()//输出函数定义
{
	cout << "姓名" << ":" << name << endl;
	cout << "学号" << ":" << name << endl;
}

int main()
{
	Student S1("张三",230021);
	S1.show();
	cout << "main()结束" << endl;

	return 0;
}

总结:系统在对象销毁(生存期结束)时,自动调用析构函数。

2.5构造和析构的调用顺序

一般情况下,调用析构函数的次序正好与调用构造函数的次序相反,也就是最先被调用的构造函数,其对应的析构函数最后被调用,而最后被调用的构造函数,其对应的析构函数最先被调用。

当然对象的构造函数和析构函数调用时机和它的生命周期是密不可分的。

下面归纳一下什么时候调用构造函数和析构函数。

(1)全局变量(生命周期:程序运行时创建,程序结束时销毁)的构造函数在所有函数(包括main函数)执行之前调用。但如果一个程序中有多个文件,而不同的文件中都定义了全局对象,则这些对象的构造函数的执行顺序是不确定的。当main函数执行完毕或调用exit函数时(此时程序终止),调用其析构函数。

(2)局部对象(在函数内定义的对象,其生命周期是进入该函数创建,函数退出结束)在进入该函数建立对象时调用其构造函数。如果函数被多次调用,则在每次建立对象时都要调用构造函数。在函数结束时调用析构函数。

(3)如果在函数中定义了静态(static)局部对象(生命周期是第一次进入该函数创建,程序退处时销毁),则只在程序第一次调用此函数建立对象时调用依次构造函数,在调用结束时对象并不被释放,因此也不调用析构函数,只在main函数结束或调用exit函数结束程序时,才调用析构函数。

(4)动态创建的对象,是调用new关键字创建函数时调用构造函数,调用delete函数销毁对象时调用析构函数。

2.5.1全局变量

class Time//时间类
{
private:
	int hour;
	int minute;
	int second;
public:
	Time(int h, int m, int s);//构造函数
	~Time();//析构函数
};

Time::Time(int h, int m, int s)//定义构造函数
{
	hour = h;
	minute = m;
	second = s;
	cout << "时间 构造函数:" << hour << ":" << minute << ":" << second << endl;
}

Time::~Time()//定义析构函数
{
	cout << "时间 析构函数:" << hour << ":" << minute << ":" << second << endl;
}

Time g_a = { 8,0,0 };
Time g_b = { 9,0,0 };
static Time g_c = { 10,10,10 };
static Time g_d = { 11,11,11 };

int main()
{
	cout << "进入main()" << endl;

	cout << "退出main()" << endl;
	return 0;
}

2.5.2局部变量

对局部对象和局部静态对象的测试如下:

//局部对象和局部静态对象
class Time//时间类
{
private:
	int hour;
	int minute;
	int second;
public:
	Time(int h, int m, int s);//构造函数
	~Time();//析构函数
};

Time::Time(int h, int m, int s)//定义构造函数
{
	hour = h;
	minute = m;
	second = s;
	cout << "时间 构造函数:" << hour << ":" << minute << ":" << second << endl;
}

Time::~Time()//定义析构函数
{
	cout << "时间 析构函数:" << hour << ":" << minute << ":" << second << endl;
}

void Fun()
{
	cout << "进入Fun()" << endl;
	Time g_a = { 8,0,0 };
	Time g_b = { 9,0,0 };
	static Time g_c = { 10,10,10 };
	static Time g_d = { 11,11,11 };
	cout << "进入Fun()" << endl;
}

int main()
{
	cout << "进入main()" << endl;
	Fun();
	cout << "退出main()" << endl;
	return 0;
}

2.5.3动态内存

动态创建对象测试如下:

//动态对象
class Time//时间类
{
private:
	int hour;
	int minute;
	int second;
public:
	Time(int h, int m, int s);//构造函数
	~Time();//析构函数
};

Time::Time(int h, int m, int s)//定义构造函数
{
	hour = h;
	minute = m;
	second = s;
	cout << "时间 构造函数:" << hour << ":" << minute << ":" << second << endl;
}

Time::~Time()//定义析构函数
{
	cout << "时间 析构函数:" << hour << ":" << minute << ":" << second << endl;
}

int main()
{
	cout << "进入main()" << endl;
	Time* pt1 = new Time{ 16,0,0 };
	Time* pt2 = new Time{ 17,0,0 };
	delete pt1;

	cout << "退出main()" << endl;
	return 0;
}

2.5.4其他情况

全局变量和局部变量夹杂情况如下,程序运行结果是什么呢?

class Time//时间类
{
private:
	int hour;
	int minute;
	int second;
public:
	Time(int h, int m, int s);//构造函数
	~Time();//析构函数
};

Time::Time(int h, int m, int s)//定义构造函数
{
	hour = h;
	minute = m;
	second = s;
	cout << "时间 构造函数:" << hour << ":" << minute << ":" << second << endl;
}

Time::~Time()//定义析构函数
{
	cout << "时间 析构函数:" << hour << ":" << minute << ":" << second << endl;
}

class Date//日期类
{
private:
	int year;
	int month;
	int day;
public:
	Date(int y, int m, int d);//声明构造函数
	~Date();//声明析构函数
}yesterday(2023,4,21);//定义全局对象

Date::Date(int y, int m, int d)//定义构造函数
{
	year = y;
	month = m;
	day = d;
	//在类Date定义的构造函数中定义类Time的对象(局部)
	Time time{ 11,11,d };
	cout << "日期 构造函数:" << year << ":" << month << ":" << day << endl;
}

Date::~Date()
{
	cout << "日期 析构函数:" << year << ":" << month << ":" << day << endl;
}

int main()
{
	cout << "进入main()" << endl;
	Date today(2023, 4, 22);

	cout << "退出main()" << endl;
	return 0;
}

2.6对象数组

定义对象数组、使用对象数组的方法与基本数据类型相似,因为类本质上也是一种数据类型。在定义对象数组时,系统不仅为对象数组分配适合的内存空间,以存放数组中的每个对象,而且还会为每个对象自动调用匹配的构造函数完成数组内每个对象的初始化工作,但数组结束时会自动调用每个对象的析构函数。

声明对象数组的格式为:

类名 数组名[数组长度];

对象数组通过下标访问具体对象,访问具体对象的数据成员的格式为:

数组名[下标].数据成员;//必须有访问权限

访问具体对象的成员函数的格式为:

数组名[下标].成员函数(实参列表);//必须有访问权限

例如:对象数组使用。

class Box//长方体类
{
public:
	Box(int len = 1, int w = 1, int h = 1)://声明有默认参数的构造函数
	length(len), width(w), height(h)
	{
		cout << "Box构造函数被调用" << endl;
	}
	~Box()
	{
		cout << "Box析构函数被调用,它的长是:" << length << endl;
	}
	int volume();//计算体积
private:
	int length;//长
	int width;//宽
	int height;//高
};

int Box::volume()//计算体积
{
	return (length * width * height);
}

int main()
{
	Box a[3] =
	{
		//定义对象数组
		Box(),//调用构造函数Box,用默认参数初始化第1个元素的数据成员
		Box(10,15),//调用构造函数Box,提供第2个元素的实参
		Box(20,30,40)//调用构造函数Box,提供第3个元素的实参
	};
	cout << "a[0]的体积是:" << a[0].volume() << endl;
	cout << "a[1]的体积是:" << a[0].volume() << endl;
	cout << "a[2]的体积是:" << a[0].volume() << endl;

	return 0;
}

程序执行结果如下:

局部变量(栈中)的数组,构造是从0下标开始往后进行,而析构是从后往前进。

总结:数组中的每个对象都自动调用构造函数和析构函数。

2.7对象指针

对象指针在使用之前必须先进行初始化。可以让它指向一个已定义的对象,也可以用new运算符动态建立堆对象。

定义对象指针的格式为:

类名 *对象指针 = &对象;
//或者
类名 *对象指针 = new 类名(参数);

用对象指针访问对象数据成员的格式为:

对象指针名 -> 数据成员

用对象指针访问对象成员函数的格式为:

对象指针名 -> 成员函数(实参列表);

例如:对象指针应用。

class Square//正方形
{
private:
	double length;//边长
public:
	Square(double len) :length(len)//构造函数
	{ }
	void show();//输出函数
};

void Square::show()
{
	cout << "正方形边长:" << length;
	cout << ",面积:" << length * length << endl;
}

int main()
{
	Square s(2.5);
	Square* s1 = &s;
	s1->show();
	Square* s2 = new Square{ 3.5 };//动态创建
	s2->show();
	delete s2;//释放动态内存

	return 0;
}

2.8this指针

this指针是一个隐含于每一个成员函数中的特殊指针。它是指向一个正操作该成员函数的对象。当对于一个对象调用成员函数时,编译程序先将对象的地址赋予this指针,然后调用成员函数。每次成员函数存取数据成员时,C++编译器将根据this指针所指向的对象来确定引用哪一个对象的数据成员。

通常this指针在系统中是隐含存在的,也可以把它显式表示出来。

例如:this指针应用。

class A
{
public:
	int get()//获得成员变量的值
	{
		return i;//不使用this指针
	}
	void set(int x)//修改成员变量的值
	{
		this->i = x;//显示利用this指针访问成员变量
		cout << "this指针保存的内存地址为:" << this << endl;//输出this指针地址
	}
private:
	int i;
};

int main()
{
	A a;
	a.set(9);
	cout << "对象a所在的内存地址为:" << &a << endl;
	cout << "对象a所保存的为:" << a.get() << endl;
	A b;
	b.set(999);
	cout << "对象b所在的内存地址为:" << &b << endl;
	cout << "对象b所保存的为:" << b.get() << endl;
	return 0;
}

每个成员函数(包括构造函数和析构函数)都有一个this指针。它指向调用对象,如果成员方法需要使用该调用对象,则可以使用this或者*this。

2.9向函数传递对象

C++语言中,对象作为函数的参数和返回值的传递方式有3中:值传递、指针传递和引用传递。

2.9.1对象作为函数参数

把实参对象的值复制给形参对象,这种传递是单向的,只从实参到形参。因此,函数对形参值做的改变不会影响到实参。

例如:对象作为函数参数应用。

class Square//正方形
{
public:
	Square(double len):length(len)//构造函数
	{ }
	void set(double len)//修改边长
	{
		length = len;
	}
	double get()//获取边长的值
	{
		return length;
	}
	void show();//输出面积
private:
	double length;//边长
};

void Square::show()//输出面积
{
	cout << "正方形面积:" << length * length << endl;
}

void Add(Square s)//只是修改形参的值,对实参没有影响
{
	double len = s.get() + 1;//len=原边长+1;
	s.set(len);//修改s的边长
}

int main()
{
	Square s(2);
	cout << "边长增加前:" << endl;
	s.show();
	Add(s);
	cout << "边长增加后:" << endl;
	s.show();
	return 0;
}

对象占用内存都比较大,一般都不传对象本身,而改为传对象引用或者对象指针。

2.9.2对象指针作为函数参数

对象指针作为参数传递的是实参对象的地址。即实参对象指针和形参对象指针指向统一内存地址,因此若参数对象指向成员数据的改变是可以影响实参数据成员的。

例如:修改上面的代码,验证对象指针作为函数参数可以修改实参的值。

class Square//正方形
{
public:
	Square(double len) :length(len)//构造函数
	{ }
	void set(double len)//修改边长
	{
		length = len;
	}
	double get()//获取边长的值
	{
		return length;
	}
	void show();//输出面积
private:
	double length;//边长
};

void Square::show()//输出面积
{
	cout << "正方形面积:" << length * length << endl;
}

void Add(Square *ps)//传递的是指针,形参可以修改实参的值
{
	double len = ps->get() + 1;//len=原边长+1;
	ps->set(len);//修改ps的边长
}

int main()
{
	Square s(2);
	cout << "边长增加前:" << endl;
	s.show();
	Add(&s);
	cout << "边长增加后:" << endl;
	s.show();
	return 0;
}

2.9.3对象引用作为函数参数

使用对象引用作为函数参数最常见,它不但有指针作为参数的优点,而且比指针作为参数更简单、更方便。引用方式进行参数传递,形参对象就是实参对象的“别名”,对形参的操作其实就是对实参的操作。

例如:用对象引用进行参数传递。

class Square//正方形
{
public:
	Square(double len) :length(len)//构造函数
	{ }
	void set(double len)//修改边长
	{
		length = len;
	}
	double get()//获取边长的值
	{
		return length;
	}
	void show();//输出面积
private:
	double length;//边长
};

void Square::show()//输出面积
{
	cout << "正方形面积:" << length * length << endl;
}

void Add(Square&s)//形参是引用,可以改变实参的值
{
	double len = s.get() + 1;//len=原边长+1;
	s.set(len);//修改ps的边长
}

int main()
{
	Square s(2);
	cout << "边长增加前:" << endl;
	s.show();
	Add(s);
	cout << "边长增加后:" << endl;
	s.show();
	return 0;
}

2.9.4三种传递方式比较

(1)值传递是单向的,形参的改变并不会引起实参的改变。而指针和引用传递是双向的,可以将改变由形参“传给”实参。

(2)引用是C++中的重要概念,引用的主要功能是传递函数的参数和返回值。学完这三种传递方式后会发现“引用传递”的性质像“指针传递”,而书写方式像“值传递”。

(3)指针能够毫无约束地操作内存中的任何东西。指针虽然功能强大,但是用起来比较困难,所以如果的确只需要借用一下某个对象的“别名”,建议使用“引用”,而不要用“指针”。

(4)使用引用作为函数参数与使用指针作为函数参数相比,前者更容易使用、更清晰。

例如:3种传递方式对比:

//值传递
void increasel(int n)
{
	cout << "值传递," << &n << endl;
	n++;//修改形参的值
}

//引用传递
void increase(int& n)
{
	cout << "引用传递," << &n << endl;
	n++;
}

//指针传递
void increase(int* n)
{
	cout << "指针传递," << &*n << endl;
	*n = *n + 1;
}
int main()
{
	int n = 10;
	cout << "&n=" << &n << ",n=" << n << endl;
	increasel(n);
	cout << "值传递,新n=" << n << endl;
	increase(n);
	cout << "引用传递,新n=" << n << endl;
	increase(&n);
	cout << "指针传递,新n=" << n << endl;
	return 0;
}

        说明:从代码效率上看,用对象值传递的方式的效率相对低一些,它需要创建新的对象来接收实参传来的值,用指针或者引用效率更高。

        引用比指针使用起来更加简洁,方便,在C++中更常用。当然如果实参数据不能被修改,可以(应该)在前面加上const。

总结:在C++中引用传递是使用的最多的方式,如果数据不能修改请在参数前加const。

2.10对象的拷贝构造和赋值

2.10.1拷贝构造函数

        如果一个构造函数的第一个参数是类本身的引用,且没有其他参数(或者其他的参数都有默认值),则该构造函数为拷贝构造函数。

        拷贝(复制)构造函数:利用同类对象构造一个新的对象

        1.函数名和类同名(构造函数)

        2.没有返回值(构造函数)

        3.第一个参数必须是类本身的对象const引用,可以有其它的参数,但其他参数必须有默认值,注意一般都只有一个参数

        4.不能重载

class Foo
{
public:
	Foo();			  //默认构造函数
	Foo(const Foo& f);//拷贝构造函数
};

        拷贝构造函数是一种特殊的构造函数,其形参是本类对象的引用,且这个参数几乎总是const的引用。其作用是使用一个已经存在的对象去初始化同类的一个新对象。如果我们不定义这个函数,系统会生成一个默认的拷贝构造函数,它的作用是从给定对象中依次将每个非static成员拷贝到正在创建的新对象中。

        如果类没有实现拷贝构造函数,它自动生成一个默认的拷贝构造函数,默认的完成分内的事情(1.为每个成员变量分配内存,2.每个成员变量赋值)

        拷贝构造函数的参数采用引用方式。如果是非引用,则调用用于也不会成功:为了调用拷贝构造函数,我们必须复制它的实参,但为了复制实参,我们又需要调用拷贝构造函数,如此无线循环。

        拷贝构造函数的特征:

        (1)拷贝构造函数的名字与类名相同,并且没有返回值;

        (2)拷贝构造函数只有一个参数,或者其它的参数都有默认值;

        (3)每个类都有一个拷贝构造函数。如果你没有定义拷贝构造函数,系统会自动生成拷贝构造函数。

        调用拷贝构造函数的情况有以下几种:

        (1)显式使用一个对象初始化另一个对象。

        (2)对象作为函数实参传递给一个非引用类型的形参。(只在笔试面试中出现,实际编码要求传引用)

        (3)返回类型为非引用类型的函数返回一个对象。

例如:拷贝构造函数应用。

        设计一个复数类,两个数据成员分别表示复数的实部和虚部。定义两个构造函数,一个普通构造函数,一个拷贝构造函数。定义add函数完成两个复数的加法。

class Complex
{
public:
	Complex(double r, double i);
	Complex(const Complex& c);
	Complex add(Complex c);//加法
private:
	double real;//实部
	double image;//虚部
};
Complex::Complex(double r, double i) :real(r), image(i)
{
	cout << "构造函数,实部:" << real << ",虚部:" << image << endl;
}
Complex::Complex(const Complex& c)
{
	real = c.real;
	image = c.image;
	cout << "拷贝构造函数,实部:" << real << ",虚部:" << image << endl;
}

Complex Complex::add(Complex c)
{
	Complex y(real + c.real, image + c.image);//构造函数
	return y;//返回值为类对象,会调用拷贝构造函数
}
void f(Complex n)//参数是类对象,会调用拷贝构造函数
{
	cout << "f(Complex n)" << endl;
}
int main()
{
	Complex a(3, 4);//调用构造函数
	Complex b(6.5, 7.5);//调用构造函数
	Complex c(a);//拷贝构造函数
	Complex d = c;//拷贝构造函数,注意和下一节的赋值区分开

	f(b);//拷贝构造函数
	c = a.add(b);//拷贝构造函数

	return 0;
}

程序分析:

1.第33,34行调用构造函数,创建了两个复数类对象a和b。输出第1,第2行。

2.第35行,Complex c(a),用一个已知对象初始化另一个对象,系统调用拷贝构造函数,输出第3行。

3.第36行,Complex d =c;利用c初始化对象d,这一句看似=赋值,其实还算调用了拷贝构造函数,因为这里还算初始化过。这两种写法是等价的Complex d=c等同Complex d(c)等同Complex d{c},这种写法对于内置类型也是一样的,int a =10等同int a(10)等同int a{10}。

4.第38行,f(b),将实参b传给形参n,因为形参是非引用的类对象,调用拷贝构造函数。

5.第38行,c=a.add(b),首先实参b传递给非引用形参c会调用构造函数,接着在add函数中定义了一个的复数类对象y(24行),系统会调用构造函数。

6.最后,函数add的返回值是一个非引用对象,系统会创建一个临时对象,将局部对象y赋值给临时对象,这时也要拷贝构造函数。

2.10.2赋值

        同类的对象之间可以互相赋值,即一个对象的值可以赋值给另一个对象。对象之间的赋值通过“=”进行。默认就是把一个对象所有非static数据成员的值依次赋值给另一个对象。

        对象赋值的一般形式为:

对象名1=对象名2;

注意:对象名1和对象名2必须是属于同一个类的两个对象。。

Person p1,p2;//定义两个同类的对象
//……         //这里省略p2初始化的过程
p1=p2;       //将p2各数据成员的值赋给p1

例如:对象赋值

class Cuboid
{
public:
	Cuboid(int len = 0, int wid = 0, int hei = 0);//有默认参数的构造函数
	void show();//输出每个成员变量的值
private:
	int length;//长
	int width; //宽
	int height;//高
};

Cuboid::Cuboid(int len, int wid, int hei)
{
	length = len;
	width = wid;
	height = hei;
}

void Cuboid::show()
{
	cout << "长=" << length << ",宽=" << width << ",高=" << height << endl;
}

int main()
{
	Cuboid Cub1, Cub2{ 10,20,30 };//定义两个对象Cub1和Cub2
	cout << "Cub1的:";
	Cub1.show();
	cout << "Cub2的:";
	Cub2.show();

	Cub1 = Cub2;//将Cub2的值赋给Cub1
	cout << "Cub1=Cub2后:" << endl;
	cout << "Cub1的:";
	Cub1.show();

	return 0;
}

        说明:

        (1)对象的赋值只对其中的数据成员赋值,不对成员函数赋值。每个对象的数据成员占用独立的存储空间,不同对象的数据成员占有不同的存储空间,赋值的过程是将一个对象的数据成员在存储空间的值复制给另一个对象的数据成员的存储空间。而不同对象的成员函数是同一个函数代码段,不需要、也无法对它们赋值。

        (2)类的数据成员中不能包括动态分配的数据,否则在赋值时可以随时出现意想不到的严重后果。

        总结:

         如果类的数据成员有指针,则一定要实现如下函数

        ·构造函数(如果没有,会出现野指针)

        ·拷贝构造函数(如果没有,会出现浅拷贝)

        ·重载 = 符号(如果没有,会出现浅拷贝

        ·析构函数(如果没有,会出现内存泄漏)

2.11组合类

        类的数据成员不但可以是基本类型,也可以是其他类的对象。

        组合类就是一个类包含其他类的对象作为该类的数据成员。

        当组合类创建对象时,其中包含的各个数据成员对象应首先被创建。因此, 在创建类的对象时,既要对本类的基本类型数据成员进行初始化,同时也要对数据成员对象成员进行初始化,同时也要对数据成员对象成员进行初始化。

        组合类构造函数的定义格式为:

类名::类名(形参表):成员对象1(形参表),成员对象2(形参表),……
{
    //类的初始化
}

需要注意以下几点:

(1)类的构造函数,不仅要考虑对本类数据成员的初始化工作,而且也要考虑成员对象的初始化工作。

(2)在创建一个组合类的对象时,不仅它自身的构造函数将被调用,且其成员对象的构造函数也将被调用。这时构造函数调用的顺序为:

·调用成员对象的构造函数,调用顺序按照成员对象在类的声明中出现的先后顺序依次调用(考点),与初始化表中顺序无关;

·执行本类构造函数的函数体;

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

(3)若调用缺省构造函数(即无形参的),则成员对象的初始化也将调用相应的缺省构造函数。

(4)组合类同样有拷贝构造函数。若无则调用默认的拷贝构造函数。

例:组合类的应用

//组合类的应用




class Date
{
public:
	Date(int y, int m, int d) :year(y), month(m), day(d)//构造函数
	{
		cout << "Date构造函数,对象的地址:" << this << endl;
	}
	//拷贝构造函数
	Date(const Date& date) :year(date.year), month(date.month), day(date.day)
	{
		cout << "Date拷贝构造函数,对象的地址:" << this << endl;
	}
	~Date()
	{
		cout << "Date析构函数,对象的地址:" << this << endl;
	}
private:
	int year;//年
	int month;//月
	int day;//日
};

class Student
{
public:
	Student(string n, int i, int y, int m, int d) :name(n), id(i), bir(y, m, d)
	{
		cout << "Student构造函数,对象的地址:" << this << endl;
	}
	Student(const Student& s): name(s.name), id(s. id), bir(s.bir)
	{
		cout << "Student拷贝构造函数,对象的地址:" << this << endl;
	}
	~Student()
	{
		cout << "Student析构函数,对象的地址:" << this << endl;
	}
private:
	string name;//姓名
	int id;		//学号
	Date bir;	//出生年月
};

int main()
{
	Student stu1{ "张三",12345,2004,6,13 };
	cout << "stu1的地址:" << &stu1 << endl;

	Student stu2(stu1);
	cout << "stu2的地址:" << &stu2 << endl;

	return 0;
}

执行结果如下:

分析:

1.第50行,定义对象stu1,需要调用Student的构造函数,但由于它包含Date类成员,所以在调用Student的构造函数前先调用Date的构造函数,然后再调用Student本身的构造函数。

2.第53行,通过stu1拷贝构造stu2,这是先调用其成员对象的拷贝构造函数,然后再调用Student本身的拷贝构造函数。

3.析构函数的调用刚好和构造函数的调用过程相反。

例如:组合类成员对象构造的顺序

class Date
{
public:
	Date(int y, int m, int d) :year(y), month(m), day(d)
	{
		cout << "Date构造函数" << endl;
	}
	~Date()
	{
		cout << "~Date析构函数" << endl;
	}
private:
	int year;  //年
	int month; //月
	int day;   //日
};

class Address
{
public:
	Address(string p, string c) :province(p), city(c)
	{
		cout << "Address构造函数" << endl;
	}
	~Address()
	{
		cout << "~Address析构函数" << endl;
	}
private:
	string province;//省份
	string city;//市
};

class Student
{
public:
	Student(string n, int i, int y, int m, int d, string p, string c) :addr(p, c), name(n), id(i), bir(y, m, d)
	{
		cout << "Student构造函数" << endl;
	}
	~Student()
	{
		cout << "~Student析构函数" << endl;
	}
private:
	string name;//姓名
	int id;//学号
	Date bir;//出生年月
	Address addr;//家庭住址
};

int main()
{
	Student stu1{ "张三",12345,2004,6,13,"陕西省","西安市" };

	return 0;
}

  • 24
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值