C++类和对象补充

Posted 蓝乐

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++类和对象补充相关的知识,希望对你有一定的参考价值。

一. 再看构造函数

我们之前已经了解了构造函数的基本内容,那么这里我们将深入认识构造函数。

1.函数体内赋初值

class Date
{
public:
 Date(int year, int month, int day)
 {
 _year = year;
 _month = month;
 _day = day;
 //可以进行多次赋值,但一般不这么做
 _year = 1;
 }
 
private:
 int _year;
 int _month;
 int _day;
};

首先,对于构造函数体内的赋值我们不能称之为初始化。首先我们要理解:初始化只能初始化一次,而构造函数体内可以多次赋值。那么对象成员变量的初始化是在什么时候进行的呢?这就要接下来要介绍的初始化列表要做的事了。

2.初始化列表

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

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

几点注意

  1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
  2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
    (1)const成员变量:由于const变量初始化之后就不能更改,因此需在初始化列表进行初始化。
    (2)引用成员变量:引用成员变量只能作为一个变量的引用,一旦初始化,就不能再作为其他变量的引用,因此引用变量也只能再初始化列表初始化。
    (3)自定义类型成员变量(没有默认构造函数情况下):由于没有默认构造函数时,自定义类型变量是不能初始化的,此时程序也无法编译,因此没有默认构造函数的自定义类型成员变量必须在初始化列表进行初始化。
class B
{
public:
	B(int i)
		:_i(i)
	{

	}
private:
	int _i;
};

class A
{
public:
	A(int a, int& b, int bb)
		:_a(a)
		,_b(b)
		,_bb(bb)
	{

	}
private:
	const int _a;//const成员变量
	int& _b;//引用成员变量
	B _bb;//自定义成员变量
};
  1. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。比如下面代码的执行结果:
class B
{
public:
	B()
	{
		cout << "B()" << endl;
	}
private:
	int _i;
};

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

	}
private:
	const int _a;//const成员变量
	int& _b;//引用成员变量
	B _bb;//自定义成员变量
};

int main()
{
	int n = 0;
	A a1(0, n);
	return 0;
}


可以看到,初始化列表中并没有对自定义变量_bb初始化,但程序仍然调用了自定义类型的默认构造函数。
4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关,先想想下面的代码运行结果是什么:

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();
	return 0;
}


可以看到的是,_a1为1,而_a2为随机值,这是因为在成员列表的声明中,_a2先被声明,_a1后被声明,因此初始化列表中的顺序是先_a2,后_a1。而一开始_a1为随机值,因此最终_a2为随机值。

3.explicit关键字

我们知道,对于构造函数,不仅可以构造和初始化对象,对于单个参数的构造函数,还具有类型转换的作用。
比如Date类:

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

int main()
{
	Date d1(2020);
	// 用一个整形变量给日期类型对象赋值
	// 实际编译器背后会用2019构造一个无名对象,最后用无名对象给d1对象进行赋值
	Date d2 = 2021;//explict禁止隐式类型转换,因此该句代码运行错误
}

但是Date d2 = 2021;这样的代码可读性不是很好,因此可以使用explicit关键字将这种隐式类型转换禁止。

二.static成员

C语言中我们就接触了static关键字,那么这个关键字修饰成员会怎么样呢?

1.概念

声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。像上面初始化列表中说的,静态的成员变量一定要在类外进行初始化。

2.特性

  1. 静态成员存储在静态区,为所有类对象所共享,不属于某个具体的实例
  2. 静态成员变量必须在类外定义,定义时不添加static关键字
  3. 类静态成员即可用类名::静态成员或者对象.静态成员来访问
  4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员;相对的,非静态成员函数可以通过this指针访问静态成员变量。
  5. 静态成员和类的普通成员一样,也有public、protected、private 3种访问级别,也可以具有返回值
    接下来我们来看看一道题:
    求1+2+3+…+n
    题目描述:求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
    这道题我们可以利用构造函数,由于每次实例化对象,都会调用其构造函数,因此我们可以实例化n个对象,每次初始化时计算求和即可;
class Sum
{
public:
    //调用构造函数
    Sum()
    {
        _sum += _i;
        ++_i;
    }
    //static修饰的成员函数,没有隐含的this指针,只能访问静态成员变量
    static int GetSum()
    {
        return _sum;
    }
private:
	//static修饰的成员变量为所有定义出来的类对象共有
    static int _i;
    static int _sum;
};
//静态成员变量的定义
int Sum::_i = 1;
int Sum::_sum = 0;

class Solution {
public:
    int Sum_Solution(int n) {
        Sum* p = new Sum[n];
        return Sum::GetSum();
    }
};

【注意】sizeof(类名)不计算静态成员变量的大小。比如上述代码中的sizeof(Sum)为1,是一个空类。

三.友元

友元分为友元函数和友元类,其提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。

1.友元函数

首先如果我们要重载<<(流插入)运算符,我们会发现将其定义成类成员函数将无法实现,这是因为类成员函数的第一个参数为this指针,那么我们只能将这个函数定义在类外,但是这样的话函数又不能访问类中的成员变量,那么这个时候要么在成员函数中实现访问的方法,要么就使用友元函数,使其可以访问类中成员。即:

class Date
{
	//用关键字friend在类中声明函数为Date的友元函数
	friend ostream& operator<<(ostream& out, const Date& d);
public:
	Date(int year, int month, int day)
		:_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;
	return out;
}

int main()
{
	Date d1(2021, 10, 20);
	cout << d1 << endl;
}

同理,cin也可以如此定义。
【说明】

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

2.友元类

和友元函数相似,友元类可以访问另一个类的私有成员。比如下面代码中,B作为A的友元类,可以访问A中的_a和_i。

class A
{
	//声明B为A的友元类,则在B中可以访问A中的成员
	friend class B;
public:
	A(int a)
		:_a(a)
	{

	}
private:
	int _a;
	static int _i;
};

class B
{
public:
	B(int b)
		:_b(b)
	{}
	static int Count()
	{
		A::_i++;
		return A::_i;
	}
private:
	int _b;
};

int A::_i = 0;

int main()
{
	A a1(1);
	B b1(1);
	cout << b1.Count() << endl;
	cout << b1.Count() << endl;
	return 0;
}

需要注意,友元关系是单向的,不具有交换性,比如上述代码中A不能访问B中的成员;友元关系不能传递,即B是A的友元,C是B的友元,但C不是A的友元,C就不能访问A中的私有成员。

四.内部类

顾名思义,定义在另一个类中的类就是内部类。注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。
内部类就是外部类的友元类。注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。

class A
{
public:
	class B//内部类,是A的友元类
	{
	public:
	//B可以直接访问A的成员
		void Print(const A& a)
		{
			cout << a._a << endl;
			cout << _i << endl;
		}
	};
	A(int a)
		:_a(a)
	{}
private:
	int _a;
	static int _i;
};

int main()
{
	A::B b1;//注意B的调用方式
	A a1(1);
	b1.Print(a1);
	//但A的对象不能去访问B中的成员
	a1.b1;//error
}

特性:

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

以上是关于C++类和对象补充的主要内容,如果未能解决你的问题,请参考以下文章

(转载)笨木头Lua专栏基础补充20:面向对象——类和继承

C++初阶第六篇——类和对象(下)(初始化列表+explicit关键字+static成员+友元+内部类)

C++类和对象(构造函数析构函数拷贝构造函数赋值运算符重载Const成员)详细解读

C++类和对象(构造函数析构函数拷贝构造函数赋值运算符重载Const成员)详细解读

C++类和对象(构造函数析构函数拷贝构造函数赋值运算符重载Const成员)详细解读

C++学习之旅第二站:类和对象进阶