详解C++类和对象(下篇)——用代码实践功能

目录

一,再谈构造函数

 1.1 构造函数体赋值

 1. 2 初始化列表 

1.21 自定义类型成员 

1.22  const 成员变量

1.23 引用成员变量  

1. 24 初始化列表的“坑”

1. 3 explicit 关键字 

二,static 成员

2.1 概念 

2.2 特性 

三, 友元

3. 1 友元函数

3. 2 友元类 

特点: 

3. 3 内部类(了解)

3. 31 概念

四, 匿名对象(了解)

五, 编译器对拷贝对象的一些优化

1. 传值传参

2. 传值返回

3. 隐式类型

4.  一个表达式中,连续构造+拷贝构造->优化为一个构造

5. 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造

6. 一个表达式中,连续拷贝构造+赋值重载->无法优化

结语


一,再谈构造函数

 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;
};
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值 ,而不能称作初始化。因为 初始化只能初始 化一次,而构造函数体内可以多次赋值

下面是过往的成员初始化,在该情景下则不太合适:

#include<iostream>
using namespace std;
class Time
{
public:
	Time(int hour = 10)   // 对于在函数体中初始化,必须写缺省参数,否则编译报错。
	{
		_hour = hour;
	}
private:
	int _hour;
};

class Date
{
public:
	Date(int year, int hour)    
	{
		_year = year;
		Time t(hour);
		_t = t;   
	}
private:
	int _year;
	Time _t; 
};

int main()
{
	Date a(2023,2);
	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.21 自定义类型成员 

 用初始化列表来完成,自定义类型初始化,看下面代码:

#include<iostream>
using namespace std;
class Time
{
public:
	Time(int hour = 11)   // 区别于函数内构造(见1.1代码,这里必须加),全缺省参数可加可不加。
	{
		_hour = hour;
	}
private:
	int _hour;
};

class Date
{
public:
	Date(int year, int hour)  
		:_t(hour)
	{
		_year = year;
	}
private:
	int _year = 100; // 缺省值,c++11打了个补丁,在内置类型初始化没有给值值时,给缺省值。
	Time _t;1 
};

int main()
{
	Date a(2023,2);
	return 0;
}

所以结合的函数体内部初始化与列表初始化我们可以看出:

1. 如果是函数内部初始化,赋值前还是得初始化,然后赋值,中间需要几次初始化,比较繁琐。

2. 如果在初始化列表初始化,可以不用有全缺省的默认构造函数,直接显示初始化。

结论:

  • 自定义类型成员推荐用列表初始化,没有全缺省参数的构造函数必须用列表初始化。
  • 内置类型推荐写到初始化列表,也可以写到函数体内部,两者随意。(除非为了代码好看,就需要写成函数内部初始化)

1.22  const 成员变量

       原因是 如:const   int   x 必须是在定义的地方初始化。(我是这么理解的,这跟const xx类型的权限性质有关)

class Date
{
public:
	Date(int year, const int x)
		: _z(x)
	{
		_year = year;
	}
private:
	int _year;
	const int _z;
};

int main()
{
	int x = 0;  // 用const修饰,就是想等传参;不修,则是缩小权限传参。
	Date a(2023, x);
	return 0;
}

1.23 引用成员变量  

        引用是另一个变量的“别名”,性质是不允许修改,所以必须在定义的时候初始化。 

class Date
{
public:
	Date(int year, int& x)
		: _z(x)
	{
		_year = year;
	}
private:
	int _year;
	int& _z;
};

int main()
{
	int x = 0;
	Date a(2023, x);
	return 0;
}

1. 24 初始化列表的“坑”

看下面代码会有什么结果:

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:

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

1. 3 explicit 关键字 

 首先我们查看一下以下代码:


#include <iostream>
using namespace std;

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

void test()
{
	Date x(10);   // 直接调用构造函数
	Date a = 100; // (隐式转化int->Date类型)构造一个Date类型的临时变量 + 
	              //拷贝构造 + 优化(a的拷贝构造无法查看) —> 直接调用构造函数
}

注意:关于编译器对拷贝的优化,本小节后面会讲) 

而这次的 explicit关键字用来修饰构造函数,功能是:禁止类型转化。

 explicit  Date(int year)
    {
        _year = year;
    }

所以Date a = 100,报错

二,static 成员

2.1 概念 

声明为 static的类成员称为 类的静态成员,用 static修饰的 成员变量,称之为 静态成员变量;用 static修饰成员函数,称之为 静态成员函数静态成员变量一定要在类外进行初始化
使用场景:static修饰全局变量,在类外可以被任意访问,那如何设计出一个 只能让类访问的全局变量

 比如: 面试题:实现一个类,计算程序中创建出了多少个类对象。

class A
{
public:
	A(int i)
		:_i(i)
	{
		_scout++;
	}

	static int Getscout()   // 类外无法取得static 成员, 所以需要一个类成员函数取得。
	{
		return _scout;      // 在静态区中寻找_scout
	}

	A(A& k)
	{
		_i = k._i;
		_scout++;
	}
private:
	static int _scout; // 声明。 (注意:缺省值为初始化列表提供的,而static成员是在类外定义)
	int _i = 0;
};
int A::_scout = 0;     // 初始化, A::通过类域定义----目的是突破类的限制

int main()
{
	A a(1);
	A b(2);
	A c(3);
	// 静态成员公有
	//cout << c._scout << endl;
	//cout << A::_scout << endl;     // 两种方式并非去访问类,而是为了突破类域,去静态区去寻找
	// 静态成员私有
	cout << c.Getscout() << endl;  // 这里访问成员函数,然后在静态区中寻找静态成员。
	cout << A::Getscout() << endl; // 通过类域访问static成员
}

2.2 特性 

1. 静态成员所有类对象所共享,不属于某个具体的对象,存放在静态区。
2. 静态成员变量必须在 类外定义,定义时不添加static关键字,类中只是 声明
3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问。
4. 静态成员函数 没有隐藏的 this指针,不能访问任何非静态成员,可以访问静态成员变量。(静态成员函数没有this指针,就找不到具体的对象,因此只能访问静态成员)
5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制(必要时需要用 成员函数访问私有的静态成员变量)。

三, 友元

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

3.1 友元函数

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

特点:

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

运用如下:

class A
{
public:
	friend int func(const A& a);  // 友元声明
private:
	int _i;
};

int  func(const A& a)   // 普通函数
{
	return a._i;
}

3. 2 友元类 

特点: 

  • 1. 友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
  • 2. 友元关系不能传递。(如果C是B的友元, B是A的友元,则不能说明C时A的友元。)
  • 3. 友元关系不能继承,在继承时再给大家详细介绍
  • 4. 友元关系是单向的,不具有交换性。 (比如下面Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接

访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。)

下面就是第4点的实践:

class Time
{
   friend class Date;   // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
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
{
public:
   Date(int year = 1900, int month = 1, int day = 1)
       : _year(year)
       , _month(month)
       , _day(day)
   {}
   
   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;    // 在Date中实例一个Time类对象,过去是无法在Time类外直接访问其私有成员。
};

3. 3 内部类(了解)

Java中用的比较多,而C++用的比较少,这里仅作了解。

3. 31 概念

如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类成员。外部类对内部类 没有任何优越的访问权限。(外部与内部有这平等关系)
(注意: 内部类是外部类的友元类,外部类不是内部类的友元类。参见友元类的定义, 内部类可以通过外部类的对象参数来 访问外部类中的所有成员。但是外部类不是内部类的友元。)

通过一段代码来测试其内部,外部类关系:

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

	class Time
	{
	public:
		Time(int hour = 0, int minute = 0, int secoud = 0)
			:_hour(hour)
			, _minute(minute)
			, _secoud(secoud)
		{}

		void func(Date& _d) // 内部内类,Date天生是Time的友元类,所以可以通过对象直接访问Date其内部私有成员。
		{
			_d._year = _hour;
			_d._month = _minute;
			_d._day = _secoud;
			cout << _d._year << endl;
			cout << _d._month << endl;
			cout << _d._day << endl;
		}
	private:
		int _hour;
		int _minute;
		int _secoud;

	};

    void fundate()
	{
		cout << _t._hour;  // 向内部类直接访问私有失败
	}

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

int main()
{
	Date::Time b;
	Date z;
	b.func(z);
	return 0;
}

特性:
1. 内部类可以定义在外部类的public、protected、private都是可以的。
2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
class A
{
public:

	class B 
	{
	public:
		void func()
		{
			cout << z << endl;  // 访问外部类中的静态变量
		}
	private:
		int _b = 100;
	};

private:
	int _i = 10;
	static int z;
};

int A::z = 10;
int main()
{
	A::B a;
	a.func();
}

3. sizeof( 外部类 )= 外部类,和内部类没有任何关系, 下面是验证代码
class A
{
public:
	class B
	{
	public:
	private:
		int _b;
	};
private:
	int _i;
};

int main()
{
	cout << sizeof(A); //运行可知为4字节
}

四, 匿名对象(了解)

 比如,让我们看下面代码:

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

	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
};
class Solution {
public:
	int Sum_Solution(int n) {
		//...
		return n;
	}
};
int main()
{
	A aa1;
	// 不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义
	//A aa1();
	// 但是我们可以这么定义匿名对象,匿名对象的特点不用取名字,
	// 但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数
	A();
	A aa2(2);
	// 匿名对象在这样场景下就很好用,当然还有一些其他使用场景,这个我们以后遇到了再说
	Solution().Sum_Solution(10);
	return 0;
}

五, 编译器对拷贝对象的一些优化

在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还是非常有用的。
下面是 VS在debug版本下的测试代码:
class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "构造" << endl;
	}
	A(const A& aa)
		:_a(aa._a)
	{
		cout << "拷贝构造" << endl;
	}
	A& operator=(const A& aa)
	{
		cout << "赋值构造" << endl;
		if (this != &aa)
		{
			_a = aa._a;
		}
		return *this;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
};

void f1(A aa)
{}

A f2()
{
	A aa;
	return aa;
}
int main()
{
	//1. 传值传参
	A aa1;
	f1(aa1);
	cout << endl;
	//2.  传值返回
	f2();
	cout << endl;
	//3. 隐式类型,连续构造+拷贝构造->优化为直接构造
	f1(1);
	//4. 一个表达式中,连续构造+拷贝构造->优化为一个构造
	f1(A(2));
	cout << endl;
	//5. 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
	A aa2 = f2();
	cout << endl;
	//6. 一个表达式中,连续拷贝构造+赋值重载->无法优化
	aa1 = f2();
	cout << endl;
	return 0;
}

 1. 传值传参

结果: 

2. 传值返回

class A
{
public:
	A()
	{
		cout << "构造" << endl;
	}

	A(const A& z)
	{
		cout << "拷贝构造" << endl;
	}

	~A()
	{
		cout << "析构" << endl;
	}
private:
	int _a = 1;
};

A func()
{
	A a;        // 1次构造
	return a;   // 1次拷贝构造
}

int main()
{
	A k = func(); // 1次拷贝构造,但是编译器会优化,把2次拷贝构造优化为1次
	return 0;

 可见,1次构造,1次拷贝;但为啥会这样,我们以下面的图来解释:

 3. 隐式类型

结果:

 

4.  一个表达式中,连续构造+拷贝构造->优化为一个构造

 

 结果:

5. 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造

 

 结果:

6. 一个表达式中,连续拷贝构造+赋值重载->无法优化

 

 总结: 代码优化是编译器的功能,在release版本下代码优化比debug版本优化更强,同时不同的编译器优化的程度也不同。

结语

本小节就到这里了,感谢小伙伴的浏览,如果有什么建议,欢迎在评论区评论;如果给小伙伴带来一些收获请留下你的小赞,你的点赞和关注将会成为博主创作的动力

  • 45
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 52
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 52
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值