C++类和对象(下)

目录

一、初始化列表

二、explicit关键字

三、static成员

四、友元

五、内部类

六、匿名对象

七、拷贝对象时的一些编译器优化


一、初始化列表

构造函数里面还有一个重点没有讲,就是初始化列表。

我们知道,在创建对象时,编译器通过调用构造函数给对象的各个成员变量赋初值,不论是编译器默认生成的构造函数还是我们自己实现的都是如此。

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

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

那么成员变量是什么时候初始化的呢?————就是在初始化列表中。

有人可能会问 “ 定义对象的时候成员变量不也完成定义了吗?”  ————对象实例化定义,是将对象这个整体进行定义,而对象的成员变量则是在初始化列表中定义的。

class A
{
public:
	A()
	{}
private:
	int _a;
	const int _b;
};

 我们知道在类中,_a、_b是声明,并非定义。const修饰的变量必须完成初始化,而这个初始化(也就是定义就需要在初始化列表中实现)

初始化列表的格式:

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

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

格式是固定的,但是也可以初始化列表和函数体内赋值混用。

这是我们之前写的栈的构造函数:

class Stack
{
public:
	Stack(int capacity = 4)
	{
		_a = (DataType*)malloc(sizeof(DataType) * capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			return;
		}
		_top = 0;
		_capacity = capacity;
	};
 

如果改用初始化列表:

class Stack
{
public:
	Stack(int capacity = 4)
		:_a((int*)malloc(sizeof(int) * capacity))
		,_top(0)
		,_capacity(capacity)
	{
		if (_a == nullptr)
		{
			perror("malloc fail");
			return;
		}
	};

但是这样写不太好,比如需要用memset对_a初始化的时候初始化列表就解决不了了,因此我们可以尝试混用:

class Stack
{
public:
	Stack(int capacity = 4)
		, _top(0)
		, _capacity(capacity)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			return;
		}
		memset(_a, 0, sizeof(int)* capacity);
	};

注意1:

1、每个成员变量都必须走初始化列表(不论有没有实现初始化列表),也就是说,写了初始化列表,成员变量会走,没写,也会默认去走。如果在初始化列表中显示初始化了(即初始化列表中给该成员变量赋初值了),那么就用该值初始化(函数体内修改则用修改值)。

2、如果成员变量没有在初始化列表显示初始化(即初始化列表中没有给该成员变量赋初值),对于内置类型和自定义类型分别处理:

a、内置类型

有自己实现的构造函数在函数体内初始化,没有则看有没有给变量缺省值,按缺省值初始化,否则初始化为随机值。

b、自定义类型

调用自己的默认构造函数(全缺省、无参、编译器实现的),没有就报错。

3、成员变量在初始化列表中只能出现一次(初始化只能有一次)

此外,有三种情况成员变量只能在初始化列表中赋值。

1、const修饰的(上面提到了,const修饰的变量必须完成初始化,只有在初始化列表中才能完成初始化,在构造函数体中的是赋值)

2、引用

class A
{
public:
	A(int a = 1)
		:_m(a)
	{}
private:
	int& _m;
};

3、自定义类型(没有默认构造函数的情况下)结合上述第二条b

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

class B
{
public:
	B(int c = 2)
		:_aa(11)//初始化
	{}
private:
	A _aa;//声明
};
int main()
{
	B bb;
	return 0;
}

注意2:

1、尽量使用初始化列表初始化,因为不管你是否使用初始化列表,都会先走它,并且对于自定义类型成员变量, 一定会先使用初始化列表初始化。

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

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

 这段代码结果是1和随机值,这是因为成员变量声明的时候先声明的是_a2,所以初始化列表中先初始化的也是_a2,与在初始化列表中出现的顺序无关。

二、explicit关键字

构造函数不仅可以构造初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值 的构造函数,还具有类型转换的作用,也称之为隐式类型转换:

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

int main()
{
	Date d1(2022);
	Date d2 = 2022;
	return 0;
}

 初始化对象,Date d2 = 2022这就是隐式类型转换,将整型转换成日期类。

实现过程:构造+拷贝构造

 先将整型2022传给构造的临时变量(Date类型),再将临时变量拷贝构造传给d2,其实类似于整型转换成浮点型的隐式类型转换:

 随着现在编译器发展,这一块的隐式类型转换 构造+拷贝构造已经优化为直接构造了,但是老一点的编译器还是分为两步进行。

同理,引用也存在隐式类型转换(前提是加const修饰):

	const Date& d3 = 2022;

但是这一块就不能将构造+拷贝构造优化为直接构造了,因为这里是引用,只能引用常属性的临时变量。

这是单参数的构造函数,支持隐式类型转换。多参数的如果第一个参数没有默认值(需要传参),而其他参数有默认值(不需要传参)则也是支持的:

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

int main()
{
	Date d1(2022);
	Date d2 = 2022;

	const Date& d3 = 2022;
	return 0;
}

上面是C++98所支持的隐式类型转换,在C++11后,多参数的也被支持了:

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(2022,1,1);
	Date d2 = { 2022,1,1 };

	const Date& d3 = { 2022,1,1 };
	return 0;
}

所有这些情况,在构造函数前加explicit 修饰就禁止隐式类型转换了。

三、static成员

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

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

静态成员在很多地方有作用,如统计类中构造函数调用次数,可以使用全局变量来统计,但是因为全局变量容易被修改的特性,一般不会轻易使用,所以静态成员变量就是一个不错的选择。

静态成员变量不能在类中定义,只能在里面声明。因为静态成员为所有类对象所共享,不属于某个具体的对象,它存放在静态区。也就是所有类的成员都能访问修改静态成员变量,因此如果在类里面定义相当于其他类成员访问的时候就会重新初始化了。

具体声明定义形式:

class A
{
public:
	A()
	{}
private:
	static int N;
};
int A::N = 0;

在类外定义时不用加static修饰,声明的时候加就行了。并且定义时要指定类域,否则编译器会认为是全局变量。

如果static成员变量在public中,那么访问时可以用 类 :: 静态成员  或者  对象 . 静态成员的形式。

class A
{
public:
	A()
	{}
//private:
	static int N;
};
int A::N = 0;

int main()
{
	cout << A::N << endl;
	A aa;
	cout << aa.N << endl;
	A* pa = &aa;
	cout << pa->N << endl;
	return 0;
}

当然,一般不会将成员变量放在public下,而是放在private下,那么上面的访问方式就不行了。

于是我们可以通过成员函数的方式来访问:

class A
{
public:
	A()
	{}
	int GetN()
	{
		return N;
	}
private:
	static int N;
};
int A::N = 0;

int main()
{
	A aa;
	cout << aa.GetN() << endl;
	return 0;
}

如果要不创建对象的形式调用成员函数,就需要用到静态成员函数了,在成员函数前用static修饰,这样函数参数就不存在隐含的this指针了,如此就可以在不创建对象的情况下通过类来访问函数,但是静态成员函数也不能访问类里面的非静态成员了。

class A
{
public:
	A()
	{}
	static int GetN()
	{
		return N;
	}
private:
	static int N;
};
int A::N = 0;

int main()
{
	cout << A::GetN() << endl;
	return 0;
}

还需注意一点,不论是静态成员变量还是函数,虽然是在静态区,生命周期是全局,但是作用域是类域,并且受到访问作用限定符限制(public 、private...)

四、友元

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

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

友元函数在C++类和对象(中)讲过了,C++类和对象(中)_SAKURAjinx的博客-CSDN博客

当时是以日期类为例,

class Date
{
//友元声明
friend istream& operator>>(istream& in, Date& d)
friend inline ostream& operator<<(ostream& out, const Date& d)
public:
	//函数...
private:
	int _year;
	int _month;
	int _day;
};

//输入日期
inline istream& operator>>(istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	return in;
}
 
//打印日期
inline ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
	return out;
}

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

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

1、友元函数可访问类的私有和保护成员,但不是类的成员函数。

2、友元函数不能用const修饰 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。

3、一个函数可以是多个类的友元函数 友元函数的调用与普通函数的调用原理相同。

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

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

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

3\友元关系不能传递

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

友元关系不能继承,在继承位置再给大家详细介绍。

友元可以突破封装,但也会破坏封装,因此不建议过多使用友元。

五、内部类

如果一个类定义在另一个类的内部,这个里面的类就叫做内部类。

内部类也可以说是类中类,也就是在一个类中定义的其他类。在C++中使用较少,在JAVA中使用较多。

内部类是一个独立的类, 它不属于外部类,更不能通过外部类的对象去访问内部类的成员。

来看个例子:

class A
{
public:
	class B
	{
	public:
		B()
		{}
	private:
		int _b;
	};
private:
	int _a;
};
int main()
{
	std::cout << sizeof(A) << std::endl;
	return 0;
}

 可以看到这里A类的大小不是8,而是4,可以说明内部类是独立的类,与定义在A类外面的不同之处在于受到类域及访问限定符的限制。

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


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

public:
 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.foo(A());
    
    return 0;
}

特性:

1. 内部类可以定义在外部类的public、protected、private都是可以的。

2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。

3. sizeof(外部类)=外部类,和内部类没有任何关系。

由上述特性可知,内部类是外部类的友元,但外部类不是内部类的友元,因此内部类可以访问外部类成员,但是外部类不能反过来访问内部类,因此使用内部类一定要小心,这也是c++不常使用内部类的原因。

六、匿名对象

对象的创建有很多种:

class A
{
public:
	A(int a = 1)
	{
		_a = a;
	}
private:
	int _a;
};
int main()
{
    A aa(1);
    A aa = 1;
	return 0;
}

可以直接创建,也可以用隐式类型转换的方式创建。

还可以创建匿名对象:

A(1);
A();

这样创建无需对象名,并且生命周期只在该行,这一行指令执行完毕则销毁。

那么匿名对象有什么实际价值呢?————我们来看下面的场景:

class A
{
public:
	A(int a = 1)
	{
		_a = a;
	}
	void Solution(){}
private:
	int _a;
};
int main()
{
	A aa(1);
	aa.Solution();
	return 0;
}

如果想调用类里面的函数Solution,之前我们会先创建一个对象然后再访问成员函数,这样为了访问成员函数还需要额外创建一个对象有点麻烦,现在可以用匿名对象直接访问:

	A(1).Solution();

这一行结束对象也自动销毁了,比较方便。

还有成员函数内部返回值的时候也有应用场景:

	// 1、
    A f1()
	{
		A ret(1);
		return ret;
	}
    //2、
    A f1()
	{
		return A(1);
	}

这一块还有应用,结合下面的拷贝对象时的一些编译器优化来讲。

七、拷贝对象时的一些编译器优化

之前我们简单提过这一块,像有的情况下编译器会将构造+拷贝构造优化成直接构造。

场景一:

class A
{
public:
	A(int a = 1)
	{
		_a = a;
		cout << "A()" << endl;
	}
	A(const A& d)
	{
		_a = d._a;
		cout << "A(A& d)" << endl;
	}
	~A()
	{
		_a = 0;
		cout << "~A()" << endl;
	}
private:
	int _a;
};

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

 老的编译器不会进行优化,但是较新版本的编译器一般都会进行优化,我用VS 2019测试一下上面代码:

可以看到只调用了一次构造函数,没有调用拷贝构造。

 这种优化场景是  构造+拷贝构造----->(优化为)构造

场景二:

class A
{
public:
	A(int a = 1)
	{
		_a = a;
		cout << "A()" << endl;
	}
	A(const A& d)
	{
		_a = d._a;
		cout << "A(A& d)" << endl;
	}
	~A()
	{
		_a = 0;
		cout << "~A()" << endl;
	}
private:
	int _a;
};

void func(A x)
{}

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

 调用了构造和拷贝构造,这种情况如果先创建了对象的话不好优化,因为优化都是在一个连续的过程中发生的,像上面场景一的情况构造与拷贝构造是连续动作,所以可以优化,但是这里是分开来的,所以不能贸然修改。

优化方式:

int main()
{
	func(A(1));
    func(1);
	return 0;
}

 这里可以结合上面讲的匿名对象优化。

 这种优化场景是  构造+拷贝构造----->(优化为)构造

场景三:

class A
{
public:
	A(int a = 1)
	{
		_a = a;
		cout << "A()" << endl;
	}
	A(const A& d)
	{
		_a = d._a;
		cout << "A(A& d)" << endl;
	}
	~A()
	{
		_a = 0;
		cout << "~A()" << endl;
	}
private:
	int _a;
};

A ff()
{
	A ret(1);
	return ret;
}

int main()
{
	A ret = ff();
	return 0;
}

 这里的过程如下图:

 编译器对此优化,将两个拷贝构造化为一个:

  这种优化场景是  构造+拷贝构造+拷贝构造----->(优化为)构造+拷贝构造

场景四:

A ff()
{
	return A(1);
}

int main()
{
	A ret = ff();
	return 0;
}

 

 这里编译器进行了极致优化,不创建中间对象,直接构造对象。

  这种优化场景是  构造+拷贝构造+拷贝构造----->(优化为)构造

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值