类和对象(三)

再谈构造函数

在构造函数体内赋值

在创建对象时,编译器自动调用构造函数,给对象中各个成员变量一个合适的初始值。

class Date
{
    
    
public:
	Date(int year, int month, int day)
	{
    
    
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称作为类对象成员的初始化,构造函数体中的语句只能将其称作为赋初值,而不能称作初始化。因为初始化只能初始化一次而构造函数体内可以多次赋值。

为什么下面的代码错误?

class Date
{
    
    
public:
	Date(int year, int month, int day)
	{
    
    
		_year = year;
		_month = month;
		_day = day;
		
		_N = 20;
		//报错,error C2166:左值指定const对象
		//这里报错说明到构造函数体内时,成员变量已经定义出来了,成员变量在哪里定义的呢?--初始化列表
	}	
private:
	int _year;//成员变量的声明
	int _month;
	int _day;

	const int _N;
};

int main()
{
    
    
	Date d1(2022,2,19);//对象定义/对象实例化
	return 0;
}

构造函数实际包含两步:1.先在初始化列表中定义非静态成员变量,2.在构造函数体内进行赋值操作,理解这个很重要!

初始化列表

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个括号,放在括号中的是初始值或表达式。只有构造函数(拷贝构造也是构造函数的一种)中可以使用初始化列表。

class Date
{
    
    
public:
	//构造函数
	Date(int year, int month, int day) :_year(year), _month(month), _day(day)
	{
    
    
		
	}

private:
	int _year;//成员变量的声明
	int _month;
	int _day;
};

初始化列表是非静态成员变量定义的地方,对于成员变量可以在定义的时候就进行初始化,也可以先定义后面再进行初始化,当然有些类型的成员变量除外。

类的定义是不占用空间的(存放在代码段),只有实例化时候才会为该对象开辟空间,空间中存储对象的成员数据/成员变量(属性)。

【注意】

  1. 每个成员变量在初始化列表中最多只能出现一次(因为初始化只能初始化一次,当然有些成员变量也可以不出现)

  2. 类中包含以下成员时,必须放在初始化列表位置进行初始化,因为他们都必须在定义时进行初始化,而初始化列表就是成员变量定义的地方:

  • 引用成员变量
  • const成员变量
  • 自定义类型成员(如果该类没有默认构造函数)

类中包含这三种类型成员变量时,有下面两种写法:

(1)所有成员变量都在初始化列表中进行初始化

class A
{
    
    
public:
	//不是默认构造函数
	A(int a)
	{
    
    
		_a = a;
	}
private:
	int _a;
};

class Date
{
    
    
public:
	//初始化列表是成员变量定义的地方
	Date(int year, int month, int day, int i, A aa) 
		:_year(year), //定义_year同时给初始值year
		_month(month), 
		_day(day),
		_N(10),
		ref(i),
		_aa(aa)
	{
    
    
		
	}

private:
	int _year;//成员变量的声明
	int _month;
	int _day;

	const int _N;//声明
	int& ref;	
	A _aa;
};

int main()
{
    
    
	A a(10);
	Date d1(2022,2,19,10,a);//对象定义/对象实例化

	return 0;
}

(2)对于_year,_month,_day可以不在初始化列表中进行初始化,先在初始化列表中定义,后面在函数体内再进行赋值操作也是可以的。

class Date
{
    
    
public:
	//初始化列表是成员变量定义的地方
	Date(int year, int month, int day, int i, A aa) //定义所有成员变量
		:_N(10),//定义同时进行初始化
		ref(i),
		_aa(aa)
	{
    
    
		_year = year, //赋值
		_month = month, 
		_day = day,
	}

private:
	int _year;//这里是成员变量的声明,实例化时才会为对象开空间,存储成员变量的值
	int _month;
	int _day;
	const int _N;
	int& ref;
	A _aa;
};

int main()
{
    
    
	A a(10);
	Date d1(2022,2,19,10,a);//对象定义/对象实例化
	//可以使用匿名对象
	Date d2(2022,2,20,20,A(20));

	return 0;
}

我们推荐(1)的写法。

  1. 尽量使用初始化列表初始化,因为使用初始化列表一定不会出问题,但是如果在构造函数体内赋值,对于自定义类型变量可能会出问题。

在这里插入图片描述
在这里插入图片描述
对比上面两段代码,在初始化列表中进行初始化,会调用一次拷贝构造;在函数体赋值,会调用一次构造函数进行初始化(在初始化列表中定义),然后调用一次赋值重载(在函数体内赋值)。

对于内置类型,在函数体内赋值和初始化列表中初始化都是可以的;
对于自定义类型,建议在初始化列表中进行初始化,这样更高效,对比上面的例子得出。

  1. 成员变量在类中声明的次序就是其在初始化列表中的初始化顺序,与其在初始化列表中写的先后次序无关。

下面这段代码的结果是什么?

class A
{
    
    
public:
	A(int a)
		:_a1(a)
		,_a2(_a1)
	{
    
    }
	
	void Print() {
    
    
		cout<<_a1<<" "<<_a2<<endl;
	}
	
private:
	int _a2;
	int _a1;
}
int main() {
    
    
	A aa(1);
	aa.Print();
}

A. 输出1 1
B.程序崩溃
C.编译不通过
D.输出1 随机值

答案是D。在类中,先声明_a2,后声明_a1,所以在初始化列表中先初始化_a2,后初始化_a1(在初始化之前成员变量都定义好了,定义其实是按照对象的大小开好一块空间,初始化是向空间里面填值)。

构造函数和初始化列表的区别

构造函数,其实有默认生成的初始化列表,初始化列表定义所有的成员变量,然后在函数体内对成员变量进行初始化操作;

初始化列表是在定义成员变量的同时就对其进行初始化,并且对初始化列表中成员变量的定义、初始化工作是先于构造函数体内代码的;

因为对于有些类型的变量,必须在定义时候就得进行初始化操作,所以只能在初始化列表中进行初始化(比如const常变量、引用类型、没有默认构造函数的自定义类型对象)。

explicit关键字

首先回顾一下隐式类型转换

int main()
{
    
    
	double d = 3.14;
	int i = d;//隐式类型转换 -- 相近类型,表示意义相似的类型
	//隐式类型转换会产生临时变量,临时变量具有const常属性
	
	int* p = &i;
	int j = (int)p;//强制类型转换 -- 无关类型

	return 0;
}

C++的构造函数不仅可以构造和初始化对象,对于单个参数的构造函数,还具有隐式类型转换的作用。

class Date
{
    
    
public:
	Date(int year)
		:_year(year)
	{
    
    }

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
    
    
	Date d1(2020);
	
	// 用一个整形变量给日期类型对象赋值
	// 实际编译器背后会用2021构造一个无名对象,最后用无名对象给d1对象进行赋值
	d1 = 2021;
	
	Date d2 = 2022
	//这里用一个整形变量给日期类型对象赋值 ,会有隐式类型转换,
	//本来用2022构造一个临时对象Date(2022),再用这个对象拷贝构造d2,
	//但是c++编译器在连续的一个过程中,多个拷贝构造会被优化,合二为一,
	//所以这里被优化成直接就是一个构造。
}

但是,如果我们不想让这样情况发生,怎么办?C++提供一个关键字explicit。用explicit修饰构造函数,将会禁止单参数构造函数的隐式转换,下面的代码报错

class Date
{
    
    
public:
	explicit Date(int year)
		:_year(year)
	{
    
    }
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
    
    
	Date d1(2020);
	// 用一个整形变量给日期类型对象赋值
	// 实际编译器背后会用2021构造一个无名对象,最后用无名对象给d1对象进行赋值
	d1 = 2021;
}

在这里插入图片描述

static成员

假如我们现在想计算某一个类实例化出来的对象的个数,该怎么办?

我们定义一个全局变量count,每次调用构造函数或者拷贝构造的时候,count++,这样就可以计算对象个数了。

int count;

class A
{
    
    
public:

	A(int a = 10) :_a(a)
	{
    
    
		count++;
	}
	A(const A& a):_a(a._a)
	{
    
    
		count++;
	}
private:
	int _a;
};

int main()
{
    
    
	A a1;
	A a2;
	std::cout<< count <<std::endl;
	count++;
	return 0;
}

但是有一个问题,在类的外面,我们是可以改变这个变量,那么就没有达到我们的目的。假如把count作为类的成员变量

class A
{
    
    
public:

	A(int a = 10) :_a(a)
	{
    
    
		count++;
	}
	A(const A& a):_a(a._a)
	{
    
    
		count++;
	}
private:
	int _a;
	int count;
};

int main()
{
    
    
	A a1;
	A a2;

	return 0;
}

但是这样的话,每个对象中都存储了一份自己的count变量,那么该怎么办呢?C++的类有一个static成员可以解决这个问题。

声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数

在类里面没有地方能定义静态成员变量,所以静态的成员变量一定要在类外进行定义,即使这个静态成员变量是私有的。

class A
{
    
    
public:

	A(int a = 10) :_a(a)
	{
    
    
		_sCount++;
	}
	A(const A& a):_a(a._a)
	{
    
    
		_sCount++;
	}

	//static int 

	//静态成员函数:没有this指针,只能访问静态成员变量和静态成员函数
	static int GetCount()
	{
    
    
		return _sCount;
	}

private:
	int _a;
	
	//静态成员变量属于整个类,并不是单单属于某个对象,属于所有的对象,生命周期在整个程序运行期间
	static int _sCount;//声明,私有的,只能在类里面访问
};

int A::_sCount = 0;//静态成员变量的定义并进行初始化

int main()
{
    
    
	A a1;
	A a2;
	
	//如何在类外面访问_sCount?
	//如果_sCount是公有的,可以这样访问:A::_sCount 或者 对象名._sCount
	
	//但是现在_sCount是私有的,怎么访问? -- 使用静态成员函数	
	//两种调用方式
	std::cout<<a1.GetCount()<<std::endl;
	std::cout<<A::GetCount()<<std::endl;


	return 0;
}

练习:求1+2+3+…+n,要求不能使用乘除法、if、else、for、while、switch、case关键字.

class Sum
{
    
    
public:
	Sum()
	{
    
    	
		_i++;
		_ret += _i;
	}
	static int GetCount()
	{
    
    
		return _ret;
	}
private:
	static int _i;
	static int _ret;
};

int Sum::_i = 0;
int Sum::_ret = 0;//存放和

class Solution
{
    
    
public:
	int Sum_Solution(int n)
	{
    
    
		//Sum a[n];//变长数组,如果不支持,使用new

		Sum* p = new Sum[n];//实例化n个Sum类的对象
		delete[] p;
		return Sum::GetCount();
	}
};

int main()
{
    
    
	Solution s;
	int ret = s.Sum_Solution(10);
	std::cout << ret << std::endl;
	return 0;
}

静态成员的特性

  1. 静态成员为所有类对象所共享,不属于某个具体的实例;

  2. 静态成员变量必须在类外定义,定义时不添加static关键字,初始化列表是定义非静态成员变量的地方(构造函数不写初始化列表,也会有一个默认的初始化列表来定义非静态成员变量),类中没有定义静态成员变量的地方,只能在类外面进行定义和初始化,在类内部只能获取和赋值操作;

  3. 类静态成员即可用类名::静态成员或者对象.静态成员来访问;

  4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员;

  5. 静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以具有返回值。

【问题】

  1. 静态成员函数可以调用非静态成员函数吗?不可以
  2. 非静态成员函数可以调用类的静态成员函数吗?可以

C++11的成员初始化

我们“类和对象(二)”这一章节中知道,C++98中,对于类而言,默认生成的构造函数对于内置类型成员变量不做处理,对于自定义类型成员变量会去调用它的默认构造函数,没有对内置类型和自定义类型做出统一的处理,这也是C++的构造函数为人诟病的地方。

class B
{
    
    
public:
	
};

class A
{
    
    
public:
	
private:
	int _a1;
	B _bb;
};

int main()
{
    
    
	A aa;
	
	return 0;
}

调试代码发现,对于内置类型_a1并没有进行初始化,_a1就是一个随机值。
在这里插入图片描述
那么在C++11中,为了给内置类型成员变量初始化,并且还要兼容以前版本,就打了一个补丁。C++11支持非静态成员变量在声明时进行初始化赋值,但是要注意这里不是初始化,这里是给声明的成员变量缺省值。这个缺省值的作用是:当初始化列表没有对非静态成员变量初始化时,用这个缺省值来初始化。

看下图中的代码,在类中声明成员变量时,给一个缺省值,在构造函数初始化列表中不对成员变量进行初始化,那么就会使用缺省值来进行初始化,调试代码发现,aa._a1的值是0。
在这里插入图片描述
同时,也可以给自定义类型成员变量缺省值。

class B
{
    
    
public:
	B(int b):_b(b)
	{
    
    

	}
private:
	int _b;
};

//C++打补丁
class A
{
    
    
public:
	A(int a1):_a1(a1)
	{
    
    

	}
		
private:
	//注意:这里不是初始化,这里放的都是声明
	int _a1 = 0;//给成员变量缺省值

	//给自定义类型变量缺省值
	B _bb = 10;//隐式类型转换

	B _bb2 = B(20);//匿名对象来初始化_bb2

	int* p = (int*)malloc(4*20);

	int arr[10] = {
    
    1,2,3,4,5};
	
	//static int count = 0;//错误
};

int main()
{
    
    
	A aa(10);
	
	return 0;
}

注意:静态成员变量不能在声明时给缺省值,静态成员变量的定义和初始化只能在类外面。因为非静态成员变量给缺省值,是当初始化列表中不显式对他们进行初始化时,用这个缺省值给他们初始化,但是静态成员变量的定义和初始化不能在类中进行,所以静态成员变量就不能有缺省值。

友元

//Date.h

#include <iostream>
using namespace std;

class Date
{
    
    
public:
	Date(int year = 0, int month = 0, int day = 0);

	void operator<<(ostream& out);

private:
	int _year;
	int _month;
	int _day;
};
//Date.cpp
#include "Date.h"
//运算符重载里面,如果是双操作数的操作符重载,第一个参数是左操作数,第二个参数是右操作数
void Date::operator<<(ostream& out)
{
    
    
	out << _year << "/" << _month << "/" << _day << endl;
}
//test.cpp

#include "Date.h"

int main()
{
    
    
	Date d1(2022,2,17);
	
	//cout << d1 ;//没办法使用,需要我们自己重载<<
	
	//重载后,仍没法cout<<d1这样使用
	
	d1.operator<<(cout);
	d1 << cout;//可以,但是用起来很奇怪
	//所以,<<没办法重载
	//怎么解决?---定义全局函数

	//cin >> d1;
	return 0;
}

C++对于自定义对象,没有默认重载流提取运算符<<,我们必须自己写,在运算符重载里面,如果是双操作数的操作符重载,第一个参数是左操作数,第二个参数是右操作数,所以上面的类成员函数operator<<的第一个隐藏的this指针是第一个操作数,所以只能d1<<cout这样使用,但是这样使用右比较怪异,那怎么办呢?我们可以在类外面定义一个函数来实现。这里就不得不提到友元这个概念。

什么是友元?

友元分为:友元函数和友元类。

友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用(能不用就不用)。

友元函数

回顾上面的问题:现在我们尝试去重载operator<<,然后发现我们没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以我们要将operator<<重载成全局函数。但是这样的话,又会导致类外没办法访问成员,那么这里就需要友元来解决。operator>>同理。

友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声
明,声明时需要加friend关键字

//Date.h

#include <iostream>
using namespace std;

class Date
{
    
    
public:
	Date(int year = 0, int month = 0, int day = 0);
	//声明友元函数
	friend void operator<<(ostream& out, const Date& d);

private:
	int _year;
	int _month;
	int _day;
};
//Date.cpp
#include "Date.h"

//友元函数实现---类外面的普通函数,定义不需要使用friend
void operator<<(ostream& out, const Date& d)
{
    
    
	//一个全局函数中想访问对象的私有成员变量 - 使用友元
	out << d._year << "/" << d._month << "/" << d._day << endl;
}

//test.cpp

#include "Date.h"

int main()
{
    
    
	Date d1(2022,2,17);
	
	cout<<d1;//可以正常使用
	
	return 0;
}

如何实现cout<<d1<<d2;呢?将函数返回值类型修改一下即可


friend ostream& operator<<(ostream& out, const Date& d);
ostream& operator<<(ostream& out, const Date& d)
{
    
    
	//一个全局函数中想访问对象的私有成员变量 - 使用友元
	out << d._year << "/" << d._month << "/" << d._day << endl;
	return out;
}

同理,实现cin重载

//Date.h

#include <iostream>
using namespace std;

class Date
{
    
    
public:
	Date(int year = 0, int month = 0, int day = 0);
	//声明友元函数
	friend ostream& out operator<<(ostream& out, const Date& d);

	friend istream& operator>>(istream& in, Date& d);

private:
	int _year;
	int _month;
	int _day;
};
	
istream& operator>>(istream& in, Date& d)
{
    
    
	cout << "请一次输入年月日:";
	in >> d._year >> d._month >> d._day;
	return in;
}

注意:

  • 友元函数可访问类的私有和保护成员,但它不是类的成员函数(就是一个普通的全局函数)

  • 友元函数不能用const修饰(const修饰的是非静态成员函数,真实修饰的是非静态函数的第一个隐藏参数this指针,表示this指针指向的对象不能修改)

  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制

  • 一个函数可以是多个类的友元函数

  • 友元函数的调用与普通函数的调用和原理相同

友元类

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。

  • 友元关系是单向的,不具有交换性。
    比如Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。(我是你的粉丝,我可以看到你的动态,并不代表你也是我的粉丝,你也能看我的动态;你想看我的动态,你要先成为我的粉丝,互关之后,我们才都是彼此的粉丝,都能看对方的动态)。

  • 友元关系不能传递
    如果B是A的友元,C是B的友元,则不能说明C是A的友元。

class Date; // 前置声明

class Time
{
    
    
	friend class Date; // 声明日期类为时间类的友元类,
	//则在日期类中就直接访问Time类中的私有成员变量

public:
	Time(int hour = 0, int minute = 0, int second = 0)
		: _hour(hour)
		, _minute(minute)
		, _second(second)
	{
    
    }

	void f(Date d);

private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
    
    
	friend class Time;//Time类是Date类的友元

public:
	Date(int year = 2022, int month = 1, int day = 1)
		: _year(year),
		_month(month),
		_day(day)//没有显式给自定义成员变量_t初始化,那么就会调用它的默认构造函数
	{
    
    
		_t._hour = 0;
		_t._minute = 0;
		_t._second = 0;
	}

	void SetTimeOfDate(int hour, int minute, int second)
	{
    
    
		// 直接访问时间类私有的成员变量
		_t._hour = hour;
		_t._minute = minute;
		_t._second = second;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};


void Time::f(Date d)
{
    
    
	std::cout << d._year << "-" << d._month << "-" << d._day << std::endl;
}

内部类

如果一个类定义在另一个类的内部,这个类就叫做内部类。注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。

注意:内部类就是外部类的友元类。注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员但是外部类不是内部类的友元

特性

  • 内部类定义在外部类的public、protected、private都是可以的。
  • 注意内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名
  • sizeof(外部类)=外部类,和内部类没有任何关系。

如果定义一个类,不想让别人用,就把它定义成内部类,并且是外部类的private。

//A是外部类/全局类
class A
{
    
    
private:
	static int k;
	int h;

public:
	//B是内部类
	//1、内部类B和在全局定义基本是一样的,只是它受外部类A类域限制
	//2、内部类B天生就是外部类A的友元,也就是B中可以访问A的私有或者保护,
	//但是A不能访问B的私有或者保护
	//A如果想访问B的私有,要在B中声明A为B的友元
	class B
	{
    
    
	public:
		friend class A;//声明A为B的友元

		void foo(const A& a)
		{
    
    
			std::cout << k << std::endl;//OK
			std::cout << a.h <<std::endl;//OK
		}
	private:
		int _b;
	};

	void f(B b)
	{
    
    
		//A为B的友元之后,才可以访问B的私有成员
		std::cout << b._b << std::endl;
	}
};

int A::k = 1;

int main()
{
    
    
	//B b;//错误
	A::B b;
	b.foo(A());//匿名对象A()
	std::cout << sizeof(A) << std::endl;//4
	return 0;
}

对于之前的联系:求1+2+3+…+n,要求不能使用乘除法、if、else、for、while、switch、case关键字.我们可以使用内部类来修改一下。

class Solution
{
    
    
private:
	//内部类
	class Sum
	{
    
    
	public:
		Sum()
		{
    
    
			_i++;
			_ret += _i;
		}
		static int GetCount()
		{
    
    
			return _ret;
		}
	private:
		static int _i;
		static int _ret;
	};

public:
	int Sum_Solution(int n)
	{
    
    
		//Sum a[n];//变长数组,如果不支持,使用new

		Sum* p = new Sum[n];//实例化n个Sum类的对象
		delete[] p;
		return Sum::GetCount();
	}
};


int Solution::Sum::_i = 0;
int Solution::Sum::_ret = 0;//存放和

int main()
{
    
    
	Solution s;
	int ret = s.Sum_Solution(10);
	std::cout << ret << std::endl;
	return 0;
}

可以不使用静态成员函数,将_ret定义为外部类的私有成员,内部类B是外部类A的友元,可以直接访问外部类的私有成员。

class Solution
{
    
    
private:
	//内部类
	class Sum
	{
    
    
	public:
		Sum()
		{
    
    
			_i++;
			_ret += _i;
		}

	private:
		static int _i;	
	};

public:
	int Sum_Solution(int n)
	{
    
    
		//Sum a[n];//变长数组,如果不支持,使用new

		Sum* p = new Sum[n];//实例化n个Sum类的对象
		delete[] p;
		return _ret;
	}
private:
	static int _ret;
};


int Solution::Sum::_i = 0;
int Solution::_ret = 0;//存放和

int main()
{
    
    
	Solution s;
	int ret = s.Sum_Solution(10);
	std::cout << ret << std::endl;
	return 0;
}

但是C++很少使用内部类。

关于C++面向对象其他内容,我们在C++高阶中再学习。

猜你喜欢

转载自blog.csdn.net/weixin_42836316/article/details/122992429