【C++】类和对象--初始化列表、友元、static成员、内部类

目录

1. 再谈构造函数

1.1. 构造函数体赋值

1.2. 初始化列表

1.2.1. 初始化列表的价值

1.3. explicit关键字

2. static成员

2.1. 问题引入

2.2. 特性

3. C++11成员初始化新玩法

4. 友元

4.1. 友元函数

4.2. 友元类

5. 内部类


今天我们学习C++类和对象的最后一些知识,主要是为了更加深入地理解和使用类和对象。

1. 再谈构造函数

1.1. 构造函数体赋值

我们之前将到过,类会默认一个构造函数。然后在实例化对象时,编译器就会调用这个构造函数,给成员变量赋值。

class Date
{
public:
	Date(int year = 1 , int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	//声明
	int _year;
	int _month;
	int _day;
};
 
int main()
{
	//对象
	Date d1(2025, 8, 23);
	Date d2;//未定义无参的默认构造  写成全缺省就ok
	return 0;
}

如果没有定义无参的构造函数,我们建议将构造函数写成全缺省的,因为这样不管是有参对象还是无参对象都能够使用这个构造函数

我们来看一下如果不是全缺省的情况:

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

};

int main() {
	Date d1(2025,8,23);
    //对于下面这个无参的对象,无法调用构造函数
    //因为我们只有含参的构造函数
	Date d2;
}

上面是通过构造函数进行初始化,除此之外,C++还提供了一种通过初始化列表的方式。


1.2. 初始化列表

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

我们试着使用初始化列表的形式,初始化我们的Date日期类:

class Date
{
public:
 
	//初始化列表
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{	
	}
 
private:
	//声明
	int _year;
	int _month;
	int _day;
};
 
int main()
{
	//对象
	Date d1(2025, 8, 23);
	Date d2;
	return 0;
}

1.2.1. 初始化列表的价值

从上面的代码来看,好像和使用构造函数体赋值并没有优化什么,那么为什么还要使用初始化列表呢?

这里我们就要回忆起一个知识点,有些成员变量是需要在声明时就进行初始化的!!!

因为如果需要在变量声明时就初始化的话,构造函数体赋值的方式,明显是会报错的。因为在没有调用构造函数之前,都无法进行初始化。而初始化列表就能完美解决这个问题!

Q:思考,那些成员是必须在定义的地方就进行初始化呢?

A:(a) 引用成员变量  (b)const成员变量  (c)自定义类型(无默认构造函数)

PS:这里补充说明一下为什么自定义类型(无默认构造函数)必须在定义时初始化。先要注意的是,这里并不是说所有的自定义类型,而是特指那些没有默认构造函数的自定义类型,而默认的构造函数是无参构造函数。所以在没有无参构造函数的情况下,如果不提供初始化的参数(或者说是一个无参的对象),是肯定会报错的。

class A
{
public:
	A(int a)
		:_a(a)
	{
	}
private:
	int _a;
};
 
class Date
{
public:
	
	//只能在初始化列表初始化
	Date(int year, int n, int ref,int a)
		:_n(n)
		,_ref(ref)
        //这里的_aa并没有别的意思,只是已经有了_a,避免重名
		,_aa(a)
	{
		_year = year;
	}
	 
private:
	//可以在定义时初始化;也可以定义时不初始化,后面再赋值修改
	int _year;
 
	//只能在定义的时候初始化
	const int _n;
	int& _ref;
	A _aa;
};

总结:

  1. 每个成员变量在初始化列表中只能出现一次(每个变量只能被初始化一次)
  2. 必须要使用初始化列表的成员变量
    • 引用成员变量

    • const 成员变量

    • 自定义类型成员 ( 该类没有默认构造函数 )

  3. 尽量使用初始化列表的方式,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化(可以理解为,与其让编译器去控制初始的过程,不如自己主动控制初始化

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

class OrderMatters {
private:
    int a;
    int b;
    
public:
    // 虽然写的是 : b(1), a(2)
    // 但实际初始化顺序是:先 a(2),再 b(1)(按声明顺序)
    OrderMatters() : b(1), a(2) {} 
};

1.3. explicit关键字

我们先来看一个奇妙的事情:

我定义的d2很明显是一个Date日期类,但是2025很明显是一个int类型,但是我却成功运行了这段程序,并且发现2025被初始化成了_year。这是怎么回事?

实际上,单参数构造函数(Single-argument constructor)可以用于隐式类型转换。这是C++中一个强大但需要小心使用的特性。

但是这样的话可能会出现一些不合法数据被错误的传入了构造函数,而explicit关键字就是用来限制这种类型转换的:

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

};

此时这个错误就会被检测出来,所以说explicit关键字就是用来禁止单参构造函数的隐式转换


2. static成员

2.1. 问题引入

我们通过一个问题来进入static的探讨,Q:统计类型A被创造了多少次?

//全局变量  -- 不提倡
int num = 0;
class A
{
 
};
A Func(A a)
{
	A copy(a);
	return copy;
}
int main()
{
	A a1;
	A a2 = Func(a1);
 
	cout << num << endl;
 
}

如果我们直接判断的话会比较不方便,找起来也比较麻烦。


方法一:全局变量

这里可以直接将构造函数和拷贝构造函数调用的记录打印不就好了,这里我们分别使用不同的两个全局变量来记录:

int num1 = 0;
int num2 = 0;
class A
{
public:
	A()
	{
		++num1;
	}
	A(const A& aa)
	{
		++num2;
	}
};
A Func(A a)
{
	A copy(a);
	return copy;
}
int main()
{
	A a1;
	A a2 = Func(a1);
 
	cout << num1 << endl;
	cout << num2 << endl;
}

这样确实可以得到正确的答案,但是会出现一个问题:在项目中,如果将头文件中定义全局变量的话可能发生链接错误;并且全局变量也破坏了封装性


方法二:静态成员变量

#include <iostream>
using namespace std;
class A
{
public:
	A()
	{
		++_count1;
	}
	A(const A& aa)
	{
		++_count2;
	}

	//静态成员变量属于整个类
	static int _count1;//声明
	static int _count2;

	int _a;

};
A Func(A a)
{
	A copy(a);
	return copy;
}
//定义
int A::_count1 = 0;
int A::_count2 = 0;

int main()
{
	A a1;
	A a2 = Func(a1);
	cout << sizeof(a1) << endl;

	cout << a1._count1 << endl;
	cout << a2._count1 << endl;

	cout << a1._count2 << endl;
	cout << a2._count2 << endl;

	cout << A::_count1 << endl;
	cout << A::_count1 << endl;


	return 0;
}

PS:这里可能在不同的计算机上会有不同的答案,可能是4113311(因为编译器的优化程度不同)

这里的结果中的 4 比较好理解,因为整个类中有一个int型的成员变量,_count1、_count2都是静态成员变量(被储存在静态区)。

这里还要注意一个点:就是静态成员变量在类中声明,不能在类中直接定义!!!定义时不添加static

如果静态成员变量是private私有的,需要编写访问接口:

class A
{
public:
	A()
	{
		++_count1;
	}
	A(const A& aa)
	{
		++_count2;
	}

	//访问接口
	int GetCount1()
	{
		return _count1;
	}
	int GetCount2()
	{
		return _count2;
	}



private:
	//静态成员变量属于整个类
	static int _count1;//声明
	static int _count2;

	int _a;

};
A Func(A a)
{
	A copy(a);
	return copy;
}
//定义
int A::_count1 = 0;
int A::_count2 = 0;

int main()
{
	A a1;
	A a2 = Func(a1);
	cout << a1.GetCount1() << endl;
	cout << a2.GetCount1() << endl;
	cout << a1.GetCount2() << endl;
	cout << a2.GetCount2() << endl;
	return 0;
}

如果我并没有实例化对象,但是我仍想访问这个静态成员变量:

正常情况下是会报错的,但是我们可以将这个访问接口也写成静态成员函数:

class A
{
public:
	A()
	{
		++_count1;
	}
	A(const A& aa)
	{
		++_count2;
	}

	//静态访问接口
	static int GetCount1()
	{
		return _count1;
	}
	static int GetCount2()
	{
		return _count2;
	}

private:
	//静态成员变量属于整个类
	static int _count1;//声明
	static int _count2;

	int _a;

};
A Func(A a)
{
	A copy(a);
	return copy;
}
//定义
int A::_count1 = 0;
int A::_count2 = 0;

int main()
{
	A a1;
	A a2 = Func(a1);
	cout << a1.GetCount1() << endl;
	cout << a2.GetCount2() << endl;
	cout << A::GetCount1();

	return 0;
}


2.2. 特性

  • 静态成员为所有类对象共享,不属于某一个类的实例
  • 静态变量在类中声明,在类外定义
  • 类静态成员可以使用,类名::静态成员、对象.静态成员来访问
  • 类静态成员和普通成员一样,有public、private、protected修饰
  • 静态成员函数没有this指针,所以不能访问非静态成员变量(因为非静态变量与对象肯定是对应的)

3. C++11成员初始化新玩法

C++11支持非静态成员变量在类中生命时,直接进行初始化赋值,注意这里的初始化赋值本质上是一种缺省值的形式。

class B
{
public:
	B(int b = 0)
		:_b(b)
	{
	}
private:
	int _b;
};
class A
{
public:
 
private:
	//非静态成员变量,可以在成员声明时给缺省值
	int a = 10;
	B b = 20;//隐士类型转换
	int* p = (int*)malloc(4);
};

4. 友元

友元包括友元函数和友元类。

友元突破封装性性,确实在一定程度上有灵活性。但是,友元同样增加了代码耦合性,所以友元的使用要有限制。


4.1. 友元函数

友元函数可以访问类中私有、受保护的成员变量,它是定义在类之外的普通函数,并不属于某一个类,只是需要在类中声明,声明时需要加上friend关键字:

class Date
{
	//友元函数
	friend ostream& operator<<(ostream& out, const Date& d);
public:
 
private:
	int _year;
	int _month;
	int _day;
};
ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "-" << d._month << "-" << d._day << endl;
	return out;
}

总结:

  • 友元函数可以访问类中的私有、受保护的成员变量,但是本身不是成员函数
  • 一个函数可以是多个类的友元函数
  • 友元函数的使用和普通函数原理一样
  • 友元函数不能使用const修饰(换句话说,声明友元函数时在后面加上const,语法上就是错误的
  • 友元函数不受访问修饰符的限制

4.2. 友元类

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

//友元
class Date;//Date类的声明
 
class Time
{
	//友元类
	friend class Date;
public:
	Time(int hour = 0, int minute = 0, int second = 0)
		:_hour(hour)
		,_minute(minute)
		,_second(second)
	{}
private:
	int _hour;
	int _minute;
	int _second;
};
 
class Date
{
	友元函数
	friend ostream& operator<<(ostream& out, const Date& d);
public:
	
	要访问Time类内的成员,使用友元类
	void SetDateTime(int hour,int minute,int second)
	{
		_t._hour = hour;
		_t._minute = minute;
		_t._second = second;
	}
private:
	int _year;
	int _month;
	int _day;
 
    Time _t;
};
ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "-" << d._month << "-" << d._day << endl;
	return out;
}

这段代码中我们可以发现,想要在Date类中访问Time类,就需要使用友元类。

注意:

  • 友元类的关系是单向的(例如:在Time类中声明了Date类是友元类,那么在Date类中可以访问Time类中的非公有成员变量,反之则不能)
  • 友元类的关系不具有传递性(例如:A是B的友元类,B是C的友元类,但是不能说明A是C的友元类)

5. 内部类

内部类的概念其实很简单,当一个类定义在另一个类的内部,那么这个内部的类就叫做内部类:

class A
{
private:
	static int k;
	int h;
public:
	//内部类
	class B//B天生是A的有友元
	{
	public:
		void foo(const A& a)
		{
			cout << k << endl;
			cout << a.h << endl;
		}
	private:
		int _b;
	};
};

然后我们来探讨一下这个内部类能干嘛?

我们先来看看这个内部类占用外部类的内存情况:

A a;
cout << sizeof(a) << endl;

结果令人有些吃惊,居然只有四个字节!!!

而这个内存正是外部类int h的,换句话说,内部类似乎与外部类并没有任何关系。那么它存在的意义是什么呢?

C++中这样规定,所有内部类天生是外部类的友元(也就是说,内部类可以访问外部类的成员,但是外部类并不是内部类的友元):

class A
{
private:
	static int k;
	int h;
public:
	//内部类
	class B//B天生是A的有友元
	{
	public:
		void foo(const A& a)
		{
			cout << k << endl;
			cout << a.h << endl;
		}
	private:
		int _b;
	};
	void Print(const B& b)
	{
		cout << b._b;
	}
};


int main()
{
	A a;
}

总结:

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

(本篇完)

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值