【C++】类和对象(三)

本文详细探讨了C++中的构造函数,包括初始化与赋值的区别、初始化列表的重要性以及explicit关键字的作用。此外,讲解了静态成员的特性、用法以及初始化,并介绍了成员初始化的新特性。还详细阐述了友元的概念,包括友元函数和友元类,以及它们在封装中的作用。最后,文章提到了内部类和封装在面向对象编程中的重要性。
摘要由CSDN通过智能技术生成

一、再谈构造函数

1.1 初始化与赋值

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

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 初始化列表(重要

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个 " 成员变量 " 后面跟一个放在括号中的初始值或表达式。

class Date
{
public:
	Date(int year, int month, int day)
		: _year(year)   // 成员变量初始化(定义)的地方
		, _month(month)
		, _day(day)
	{
		_year = 2020;   // 只是对成员变量赋值
		_month = 1;
		_day = 1;
	}

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

// ...
Date d(2020, 1, 1); // 实例化(定义)对象

注意

  • 初始化列表中:只能对类中 非静态的成员变量 来进行初始化。

  • 每个成员变量在初始化列表中 只能出现一次(初始化只能初始化一次)

  • 类中包含以下成员,必须 放在 初始化列表位置 进行 初始化

    1. 引用类型的成员变量(因为引用必须在声明时初始化)
    2. const 修饰的成员变量(因为常变量不允许修改,必须在定义时初始化)
    3. 自定义类型成员(该类没有「默认构造函数」)
    class A {  // A类没有默认构造函数 
    public:
    	A(int a)
    		: _a(a)
    	{}
    
    private:
    	int _a;
    };
    
    class B {
    public:
    	B(int& ref, int n, int aa)
    		: _ref(ref)
    		, _n(10)
    		, _aa(aa) // 调用A类的有参构造函数
    	{}
    
    private:
    	int& _ref;    // 引用类型的成员变量
    	const int _n; // const修饰的成员变量
    	A _aa;        // 自定义类型(没有默认构造函数)
    
    	static int _c; // 静态成员变量,所有对象共享,放在类外初始化(定义)一次就行了
    };
    
    int B::_c = 10; // 初始化(定义)静态成员变量
    
    // ...
    int ref = 10
    B b(ref, 10, 20);
    
  • 尽量使用初始化列表初始化

    • 因为不管你是否使用初始化列表,都默认为这里(构造函数体前)有初始化列表,成员变量都会在该处完成初始化。
    • 有些成员是必须在初始化列表中初始化的。
    • 初始化列表和函数体内初始化,可以混着用,互相配合。(比如带头双向循环链表的构造函数)

    对于自定义类型成员变量,一定会先使用初始化列表初始化:

    一个类中有自定义类型成员变量,实例化该类对象时,会调用该类的构造函数,通过汇编代码可看到,在该类的构造函数的函数体 {} 之前,有一个 call 命令,去调用了自定义类型的构造函数。

  • 成员变量 在类中「声明次序」就是其在初始化列表中的「初始化顺序」,与其在初始化列表中出现的先后次序无关。

  • 注意一点:

    class B {
    public:
        /* 不能这样写,_ref引用的是一个局部变量,栈帧销毁了,局部变量就没了
        B(int ref)
    		: _ref(ref)
    	{
    		_ref = 100;
    	}
        */
        
    	B(int& ref)
    		: _ref(ref)
    	{
    		_ref = 100; // _ref被改变成100,ref也会被改变成100
    	}
    
    private:
    	int& _ref;    // 引用类型的成员变量
    };
    
    int main() {
        int ref = 10;
    	B b(ref);
    	return 0;
    }
    

1.3 explicit 关键字

构造函数不仅可以构造与初始化对象,对于「单个参数」的「构造函数」,还具有「类型转换」的作用。

👉示例

class A {
public:
	A(int a)       // 单参数的构造函数
		: _a(a)
	{
		cout << "A(int a)" << endl;
	}

private:
	int _a;
};

int main()
{
	// 实例化新对象的两种写法:

	// 调用构造函数,实例化新对象a1
	A a1(10);

	// 下面本质上是一个隐式类型转换 --> 用一个int类型变量给A类型对象赋值
	// 实际上编译器,会先用20作为参数构造一个临时对象,再用临时对象拷贝构造新对象a2
	// 最终编译器进行了优化,直接用20作为参数构造新对象a2,把临时对象当成a2来用了,没有调用拷贝构造
	A a2 = 20;

	return 0;
}

总结

虽然这两种写法的运行结果是一样的,都是直接调用构造函数,但是对于编译器而言过程是不一样的。

image-20220424153008758

第二种写法隐式类型转换过程图(未被编译器优化前):

image-20220424153232595

👉示例:用 explicit 修饰构造函数,将会禁止单参构造函数的隐式转换。(了解下即可)

class A {
public:
	explicit A(int a)
		: _a(a)
	{
		cout << "A(int a)" << endl;
	}

private:
	int _a;
};

int main()
{
	A a1(10);
	A a2 = 20; // 编译器报错error:,“初始化”:无法从“int”转换为“A”

	return 0;
}

👉拓展:C++11支持多参数的隐式类型转换。同样也可以加 explicit 关键字来禁止隐式类型转换。

class A {
public:
	A(int a, int b)
		: _a(a)
        , _b(b)
	{}

private:
	int _a;
    int _b;
};

int main()
{
	A a1(10, 20);

	// 下面本质上是一个隐式类型转换
	A a2 = { 10, 20 }; // C++11支持多参数的隐式类型转换,写一个花括号把参数括起来

	return 0;
}

二、static 静态成员

👉【笔试题】:如何计算下面程序构造了多少个对象(构造 + 拷贝构造)

方法一:通过定义全局变量来统计

int sum = 0;

class A
{
public:
	A() { sum++; }
	A(const A& a) { sum++; }
};

A f(A a)
{
	A ret(a);
	return ret;
}

int main()
{
	A a1 = f(A());
	A a2;
	A a3;
	a3 = f(a2);

	cout << sum << endl; // 8

	return 0;
}

思考:全局变量就这样给暴露在外面,万一有人把全局变量给修改了呢?

方法二:static 类静态成员

class A
{
public:
	A() { _count++; }
	A(const A& a) { _count++; }
	
	static int GetCount() { return _count; } // 静态成员函数

private:
	/* 声明静态成员变量
	* 注意:
	* 存在静态区,属于整个类,所有对象共享,所以只需要放在类外初始化一次
	* 跟全局变量相比,静态成员变量受到类域和访问限定符的限制,更好的体现了封装性,别人不能轻易修改它
	*/
	static int _count;
};

int A::_count = 0; // 类外初始化(定义)静态成员变量

A f(A a)
{
	A ret(a);
	return ret;
}

int main()
{
	A a1 = f(A());
	A a2;
	A a3;
	a3 = f(a2);

	cout << A::GetCount() << endl; // 8
	cout << a1.GetCount() << endl; // 8

	return 0;
}

概念

  • static 修饰的类成员称为类的静态成员。
  • 用 static 修饰的成员变量,称之为静态成员变量;用 static 修饰的成员函数,称之为静态成员函数。
  • 静态的成员变量一定要在类外进行初始化。

特性

  1. 类静态成员,为所有类对象所「共享」,不属于某个具体的实例。
  2. 类静态成员变量,必须在「类中声明」,「类外单独定义」(定义时不添加 static 关键字)。
  3. 类静态成员,可用【类名::静态成员名】或者【对象.静态成员名】来访问。
  4. 类静态成员函数,没有隐含的 「this 指针」,不能访问任何非静态成员。
  5. 类静态成员和类的普通成员一样,也有 public、protected、private 三种访问权限,也可以具有返回值。

思考

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

回忆

  • 在C语言中:

  • static 修饰全局变量和全局函数,改变了其链接属性,只在当前文件可见。

  • static 修饰局部变量,改变了其声明周期(即改变了该变量的存储位置),变为全局。

  • 上面的特性在 C++ 中依旧有用(因为 C++ 兼容 C 的这些特性)


三、成员初始化的新玩法(C++11)

C++11 支持「非静态成员变量」在「声明时」进行初始化赋值,但是要注意这里不是初始化,这里是给声明的成员变量一个「缺省值」。

构造函数对内置类型不处理,我们可以采取给缺省值的方式来 “ 打补丁 ”

如果我们在构造函数的形参列表中给了缺省值,或者在初始化列表中显式的给了值,就不会用缺省值了。

class A
{
public:
	A(int a = 0)
        :_a(a)
    {}
    
private:
	int _a;
};

class B
{
private:
	// 非静态成员变量,可以在成员声明时给缺省值
    
    // 实例化对象时,这里实际上会被搬移到初始化列表的位置初始化
    
	int _b = 10;
	int* _p = (int*)malloc(sizeof(int) * 10);
    
    // A _aa = A(10); // 先构造再拷贝构造,但会被编译器优化成直接构造
    A _aa = 10; // 还可以这样写(因为单参数的构造函数支持隐式类型转换,把int转换成A类型)
};

int main()
{
	B b;
	return 0;
}

四、友元

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

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

友元相当于把你家的钥匙🔑给别人了。

4.1 友元函数

概念

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

特性

  • 友元函数可访问类的私有和保护成员,但不是类的成员函数。
  • 友元函数不能用 const 修饰。
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
  • 一个函数可以是多个类的友元函数。
  • 友元函数的调用与普通函数的调用和原理相同。

应用

👉思考:之前我们实现的日期 Date 类,如果想要用流提取 >> 和流插入 << 运算符来初始化和输出日期类对象,该怎么办呢?

class Date {
// ...
};

int main() {
    Date d1;
    cin >> d1;
    cout << d1;
}

提问:cin 和 cout 是什么类型的对象呢?

cin 是在 iostream 头文件中定义的 istream 类的对象

cout 是在 iostream 头文件中定义的 ostream 类的对象

image-20220424171924748

将流插入 << 运算符重载成成员函数:

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

	// 流插入 << 运算符重载函数
    // 编译器会解释为:void operator<<(Date* const this, ostream& out)
	void operator<<(ostream& out)
	{
		out << _year << "/" << _month << "/" << _day << endl;
	}

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

int main() {
	Date d1;

	// cout << d1; // 这样使用,编译器会报错
    
	// 要像这样使用才能正确输出Date类对象,但这样就搞的颠倒了,不合符运算符原来的用法和特性
	d1 << cout; // 编译器会解释为:d1.operator<<(cout); -- d1.operator<<(&d1, cout);
	d1.operator<<(cout);
    
	return 0;
}

👉上面代码出现的问题:现在我们尝试去重载 operator<<,发现没有办法将 operator<< 重载成成员函数。因为 cout 的输出流对象和隐含的 this 指针在抢占第一个参数的位置。this 指针默认是第一个参数(也即是左操作数,void operator<<(Date* const this, ostream& out))。

但在实际使用中(比如 cout << d1;),cout 对象必须要作为第一个形参,d1对象作为第二个形参,才能正常使用。所以我们要将 operator<< 重载成全局函数。但是这样的话,又会导致在类外没办法直接访问私有成员变量,那么这里可以用友元来解决。operator>> 同理。

同时,流插入 << 运算符还要支持连续输出(比如:cout << d1 << d2; ),该如何实现呢?

正确写法

class Date
{
	// 声明为友元函数
	friend ostream& operator<<(ostream& out, const Date& d);

public:
	Date(int year = 2020, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}

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

// 流插入 << 运算符重载函数
ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "/" << d._month << "/" << d._day << endl;
    return out; // 返回输出流对象out,为了支持连续输出
}

int main()
{
	Date d1, d2;
	cout << d1 << d2;
    // 先执行cout << d1,即operator<<(cout, d1),函数会返回cout对象的引用
	// cout << d1的返回值作为第一个形参,再执行cout << d2,即operator<<(cout, d2)
    
    // cout << d1; -- 编译器会解释为:operator<<(cout, d1);

	return 0;
}

👉提问:重载流插入 << 运算符,只能用友元函数,对不对?

不对,友元函数只是这里的一种解决方案,我们还可以这样写

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

    // 为了方便在类外通过公有接口,来访问私有成员
    // const修饰成员函数,保护成员变量不能被修改
	int GetYear() const { return this->_year; } 
	int GetMonth() const { return this->_month; }
	int GetDay() const { return this->_day; }
    
private:
	int _year;
	int _month;
	int _day;
};

// 流插入 << 运算符重载函数
ostream& operator<<(ostream& out, const Date& d) // const对象只能调用const成员函数
{
	out << d.GetYear() << "/" << d.GetMonth() << "/" << d.GetDay() << endl;
	return out; // 返回输出流对象out,为了支持连续输出
}

完整代码

借助友元函数看,重载「流插入 << 运算符」和「流提取 >> 运算符」:

class Date
{
	// 声明为友元函数
	friend ostream& operator<<(ostream& out, const Date& d);
    friend istream& operator>>(istream& in, Date& d);

public:
	Date(int year = 2020, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}

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

// 流插入 << 运算符重载函数
ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "/" << d._month << "/" << d._day << endl;
    return out; // 返回输出流对象out,为了支持连续输出
}

// 流提取 >> 运算符重载函数
istream& operator>>(istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	return in; // 返回输出流对象in,为了支持连续输入
}

int main()
{
	Date d1, d2;
    cin >> d1 >> d2;  // 输入日期类对象
	cout << d1 << d2; // 打印日期类对象
	return 0;
}

4.2 友元类

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

注意:

  • 友元关系是单向的,不具有交换性。

    比如下面的 Time 类和 Date 类,在 Time 类中声明 Date 类为其友元类,那么可以在 Date 类中直接访问 Time类的私有成员变量,但想在 Time 类中访问 Date 类中私有的成员变量则不行。

  • 友元关系不能传递。

    如果C 是 B 的友元,B 是 A 的友元,则不能说明 C 是 A 的友元。

class Date; // 前置声明Date类

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

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

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

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

	void SetTimeOfDate(int hour, int minute, int second) {
		// 可以直接访问Time类私有的成员变量
		_t._hour = hour;
		_t._minute = minute;
		_t._second = second;
	}

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

五、内部类

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

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

特性

  1. 内部类可以直接访问外部类中的 static、枚举成员,不需要外部类的对象 / 类名。
  2. sizeof(外部类) 计算外部类对象大小时,不考虑内部类。
  3. 内部类,跟普通类并没有什么区别,只是它定义在外部类内部,会受到外部类类域访问限定符的限制。

一般情况,需要一个内部类和外部类紧密关联时,才会用内部类,但实际中用的少,了解下即可。

class A
{
private:
	static int k;
	int h;

public:
	/* 在A类的内部定义B类 */

	class B // B天生就是A的友元
	{
	public:
		void foo(const A& a)
		{
			cout << k << endl;   // OK
			cout << a.h << endl; // OK
		}
	};
};

int A::k = 1;

int main()
{
	A::B b; // 实例化内部类B的对象
	b.foo(A());
	return 0;
}

六、再次理解封装

C++是基于面向对象的程序,面向对象有三大特性即:封装、继承、多态。

C++通过类,将一个对象的属性与行为结合在一起,使其更符合人们对于一件事物的认知,将属于该对象的所有东西打包在一起;通过访问限定符选择性的将其部分功能开放出来与其他对象进行交互,而对于对象内部的一些实现细节,外部用户不需要知道,知道了有些情况下也没用,反而增加了使用或者维护的难度,让整个事情复杂化。

封装是一个更好的管理,受规则约束的一种管理,在生活中很多方面都体现着封装性。

比如火车站:有咨询系统,售票系统,安检系统,安保系统,卫生系统,铁路系统等等,这些系统相互配合,让大家坐火车有条不紊的进行。如果没有这些系统,变成开放性的站台呢?火车站没有围墙,大家都去扒火车,乘车也没有规矩,都去抢座位,无法想象这是怎样一番场景。


七、再次理解面对对象

面向对象其实是在模拟抽象映射现实世界。

数据对数据的操作方法放在一起,作为一个相互依存的整体——对象。 对同类对象抽象出其共性,形成类。类中的大多数数据,只能用本类的方法进行处理。 类通过一个简单的外部接口与外界发生关系,对象对象之间通过消息进行通信。

image-20220425175122684

评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值