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

const成员函数

【概念】:将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改

  • const 修饰Date类的Print成员函数,Print隐含的this指针由 Date* const this 变为 const Date* const this
  • 也就是this指针指向的内容(成员变量),不能修改。
class A {
public:
	void Print()
	{
		cout << _a << endl;
	}
private:
	int _a = 10;
};

int main(void)
{
	A a;
	a.Print();
	return 0;
}

在这里插入图片描述

  • 可以看到,这样的普通对象a是可以调用printf()成员函数
  • 但此时我在实例化这个对象aa时在类的前面加上一个constt修饰对象,编译却报出了错误说了一些有关this指针的内容
    在这里插入图片描述
  • 既然和this指针有关的报错,那我们是否可以从这个点入手去分析一下呢?要知道一个对象去调用当前类中的成员函数时会传递它的地址给到成员函数中的隐藏形参this指针,然后在内部this指针就可以通过不同的对象地址h去访问不同地址空间中对应的成员变量
void Print(A* this)

  • 此时就可以看到了,我们把const修饰的对象的地址传给一个没被const修饰的指针,也就是老大都没有修改指向内容的缺陷,小弟有修改内容的权限吗?

在这里插入图片描述

  • 这里就出现了权限放大,编译器就会报错,我们说过权限可以缩小但是不能放大。
  • 里补充一个知识点,权限放大只发生于指向同一块内容的权限问题。注意:这
const int i = 10;
int j = i;
  • 这种不是权限放大,i 指向10,j指向i是两块不同的空间

  • 这时候就有人说了,既然找到问题,那么给形参位置的this指针前面加上const就行了。

  • 此时问题又出现了,我们在类和对象(上)讲过,C++规定不能在实参和形参的位置显⽰的写this指针。

  • 所以直接加上不行的,这时候C++规定在参数列表后面加上const就等于用const修饰this指针指向的内容,如:const A* const this.

  • 注意:A*后面的cosnt是this指针默认加上的,this指针本身不能移动

void Print() const
{
	cout << _a << endl;
}

  • 来看一下运行结果就可以发现没有报错了,这就是权限保持
    在这里插入图片描述

  • 注意:const指针只能用于类的成员函数,不能用于普通函数,因为const是专门用于修饰this指针,而this指针只有类的成员函数才有

    这里我们给出两个问题:

  • const成员函数内可以调用其它的非const成员函数吗?

  • 答案是不可以,同样也是权限放大,万一这个非const成员函数去修改了成员变量的内容就会出问题了
    在这里插入图片描述

  • 非const成员函数内可以调用其它的const成员函数吗?

  • 答案是可以,我们说了权限不能放大,但是可以缩小,,只有调用函数不去修改对象即可

取地址及const取地址操作符重载

  • 类类型的对象进行运算操作的运算符都需要重载,取地址也不例外

class Date
{
public:
	Date* operator&()
	{
		return this;
	}
	const Date* operator&() const
	{
		return this;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};

  • 可以看到,就是去打印一下这个对象的地址,通过函数实现可以看到return this,那返回的也就是当前对象的地址

日期类的优化

  • 刚刚我们讲了const成员函数,那么我们日期类里面的成员函数,很多都不需要修改this指针指向的内容,这样我们就可以在成员函数后面加const修饰
    例如像下面的这些,都是可以声明为const成员函数的【注意在定义部分也要加上】
//获取当月的天数
int GetMonthDay(int year, int month); const

//等于==
bool operator==(const Date& d) const;

//不等于!=
bool operator!=(const Date& d) const;

//小于<
bool operator<(const Date& d) const;

//小于等于==
bool operator<=(const Date& d) const;

//大于>
bool operator>(const Date& d) const;

//大于等于>=
bool operator>=(const Date& d) const;

//日期 + 天数
Date operator+(int days) const;

//日期 - 天数
Date operator-(int days) const;

//日期 - 日期
int operator-(const Date& d) const;		//构成重载

再谈构造函数

  • 没想到吧,构造函数还没有学完哈哈。

初始化列表

引入

  • 我们知道,对于下面这个类A的成员变量_a1和_a2属于【声明】,还没有在内存中为其开辟出一块空间以供存放,真正开出空间则是在【定义】的时候,那何时定义呢?也就是使用这个类A去实例化出对象的时候
class A {
public:
	int _a1;	//声明
	int _a2;
};

  • 如果现在我在类A中加上一个const成员变量的话,初始化的时候似乎就出现了问题
    在这里插入图片描述
  • 在搞清楚上面的问题之前你要明白const修饰的变量有哪些特点
  • 可以看到,我们在定义cosnt修饰的变量同时必须要对const修饰的变量进行初始化。
    在这里插入图片描述

现在我们就可以来聊聊有关上面的成员变量_x为什么没有被初始化的原因了👇

  • 之前有讲过,若是我们自己不去实现构造函数的话,类中会默认提供一个构造函数来初始化成员变量,对于【内置类型】的变量不会处理,对【自定义类型】的变量会去调用它的构造函数。那么对于这里的_a1、_a2、_x都属于内置类型的数据,所以编译器不会理睬,可是呢const修饰的变量又必须要初始化,这个时候该怎么办呢╮(╯▽╰)╭
  • 这就可以用我们的初始化列表了

初始化的概念区分

  • 在了解【初始化列表】前,你要先知道初始化的真正含义是什么
  • 概念:在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

  • 上面这个Date类是我们之前写过的,这里有一个它的有参构造函数,虽然在这个构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化。构造函数体中的语句只能将其称为【赋初值】,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。

语法格式及使用

  • 【初始化列表】:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式
  • 下面就是它的具体用法,这样便可以通过外界传入一些参数对年、月、日进行初始化
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(void)
{
	Date d(2023, 3, 30);
	return 0;
}

调试一下:
在这里插入图片描述

知识中转站:缺省值初始化。

  • 在C++11中,如果说我们没有使用初始化列表进行初始化成员变量,其实还可以再声明的同时给一个缺省值来初始化成员变量。
class Date
{
public:
	Date(int year = 2, int month = 2, int day = 2)
	{
		
	}
private:
	int _year = 1;
	int _month = 1;
	int _day = 1;
};

在这里插入图片描述

成员变量走初始化列表的逻辑

  • 还是看回到我们上面的这个类A,知道了【初始化列表】这个东西,此时就不需要再声明的部分给缺省值了,直接使用初始化列表即可。不过可以看到,对于_a1和_a2我给到了缺省值,写了初始化列表后,它们还会被初始化吗?
class A {
public:
	A()
		:_x(1)
	{}
private:
	int _a1 = 1;	//声明
	int _a2 = 1;

	const int _x;
};

也通过调试来看一下在这里插入图片描述

  • 可以看到,即使在初始化列表没有给到_a1和_a2的初始化,还是会通过给到的默认缺省值去进行一个初始化。

  • 所以不管是否显式在初始化列表写,编译器都会为每个变量在初始化列表进行初始化

好,接下去难度升级,请问初始化列表修改成这样后三个成员变量初始化后的结果会是什么呢? 会是1、2、1吗?

class A {
public:
	A()
		:_x(1)
		,_a2(1)
	{}
private:
	int _a1 = 1;	//声明
	int _a2 = 2;

	const int _x;
};

一样通过调试来看看
在这里插入图片描述

  • 可以看到,最后_a2是1,答案就是1 1 1
  • 这里要明确的一个概念是,缺省参数只是一个备胎,若是我们没有去给到值初始化的话,编译器就会使用这个初始值,若是我们自己给到了明确的值的话,不会去使用这个缺省值了

接下去难度继续升级,请问下面这样初始化后的结果是多少?

class A {
public:
	A()
		:_x(1)
		,_a2(1)
	{
		_a1++;
		_a2--;
	}
private:
	int _a1 = 1;	//声明
	int _a2 = 2;

	const int _x;
};

  • 可以看到对于构造函数我不仅写了【初始化列表】,而且在函数体内部还对_a1和_a2进行了++和- -
    在这里插入图片描述
  • 无论是否有缺省值和初始化列表他们针对初始化成员变量,构造函数内有语句就会执行

如图:
在这里插入图片描述

注意事项

  • 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
  • 在这里插入图片描述
  • 类中包含以下成员,必须放在初始化列表位置进行初始化:
  • const成员变量
  • 这个在前面说了,cosnt成员变量和构造函数不对内置类型进行处理发生冲突,const成员变量必须初始化。
  • 引用成员变量
  • 我们在说引用的时候也说了,引用在定义的时候也必须初始化,,它必须要引用一个值
    在这里插入图片描述
  • 没有默认构造的自定义类型成员(写了有参构造编译器就不会提供默认构造)
  • 此时,我又写了一个类B,将它定义出的对象作为类A的成员变量,在类B中,有一个无参的默认构造,也写了相关的初始化列表去初始化_b
class B {
public:
	B()
		:_b(0)
	{}
private:
	int _b;
};

class A {
public:
	A()
		:_x(1)
		,_a1(3)
		,_a2(1)
		,_z(_a1)
	{
		_a1++;
		_a2--;
	}
private:
	int _a1 = 1;	//声明
	int _a2 = 2;

	const int _x;
	int& _z;
	B _bb;
};

  • 通过调试来观察就可以看到,完全符合我们前面所学的知识,若是当前类中有自定义类型的成员变量,那在为其进行初始化的时候会去调用它的默认构造函数
    在这里插入图片描述
  • 但是现在我对这个构造函数做了一些改动,将其变为了有参的构造函数,此时编译时就报出了【没有合适的默认构造函数可用】
  • 我们知道默认构造有:无参、全缺省和编译器自动生成的,都是不需要我们手动去调的。可以看到若是我在这里将其改为全缺省的话,就不会出问题了,因为它属于默认构造函数
    在这里插入图片描述

那对于有参构造该如何去初始化呢?

  • 还是可以利用到我们的【初始化列表】
    在这里插入图片描述
    在这里插入图片描述
  • 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化
  • 下面我们在看看我们的用栈实现队列。
typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 10) 	//全缺省构造
	{
		cout << "Stack()构造函数调用" << endl;
		_array = (DataType*)malloc(capacity * sizeof(DataType));
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}
		_size = 0;
		_capacity = capacity;
	}
	//....
private:
	DataType* _array;
	size_t _size;
	size_t _capacity;
};


class MyQueue{
public:
	//默认生成构造函数

private:
	Stack _pushST;
	Stack _popST;
	size_t _t = 1;
};

int main(void)
{
	MyQueue mq;
	
	return 0;
}

  • 对于【内置类型】不做处理,不过我这里给到了一个缺省值,对于【自定义类型】会去调用它的默认构造
  • 那此时我若是将这个默认构造(全缺省构造)改为有参构造吗,它还调得动吗?
Stack(size_t capacity)

在这里插入图片描述

  • 此时就可以使用到我们本模块所学习的【初始化列表】了,将需要定义的值放在初始化列表,相当于就是为Stack类传入了一个有参构造的参数,不过对于没有写在这里的_t,依旧会使用我给到的初始值1

可以通过调试再来看看

在这里插入图片描述

  • 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
  • 最后再来看第四点,你认为下面这段代码最后打印的结果会是多少呢?1 1 吗?
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();
}

但是结果是:
在这里插入图片描述

  • 通过调试可以发现,似乎是先初始化的_a2再去初始化的_a1,那若是一开始使用_a1去初始化_a2的时候,那_a2就会是一个随机值,但是_a1却使用传入进来的形参a进行了初始化,那它的值就是1
    在这里插入图片描述
  • 所以说,我们说初始化列表的初始化顺序应该与声明成员变量的顺序保持一致

类型转换

  • C++⽀持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。

单参构造函数

  • 还是老朋友,我们通过下面这个日期类进行讲解
class Date
{
public:
	Date(int year)
		:_year(year)
	{}

private:
	int _year;
	int _month = 3;
	int _day = 31;
};

对于下面的d1很清楚一定是调用了有参构造进行初始化,不过对于d2来说,也是一种构造方式


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

依旧通过调试来看就会非常清晰,这种写法也会去调用构造函数
在这里插入图片描述

  • 在操作符章节,我们就讲过,不同类型之间运算会进行隐式转换。
int i = 1;
double d = i;

  • 这种i就会转换double
  • 那对于这个d2其实也是一样,2023会先去构造一个临时对象,这个临时对象的类型是[Date]把它里面的year初始化为2023,然后再通过这个临时对象进行拷贝构造给到d2,
    在这里插入图片描述
  • 有人问,不是说构造函数有初始化列表吗?拷贝构造怎么去初始化呢?
  • 可是拷贝构造也是特殊的构造函数,所以拷贝构造就是构造函数,既然是构造函数就可以使用初始化列表
  • 这里要注意的是我们vs编译器进行类型转换的时候,并没有去调用拷贝构造,这其实是vs编译器的优化,他决定太麻烦了,就只进行了构造,但是一些低版本的会进行拷贝构造。
    在这里插入图片描述

但若是你不想让这种隐式类型转换发生怎么办呢?此时就可以使用到C++中的一个关键字叫做explicit

  • 它加在构造函数的前面进行修饰,有了它就不会发生上面的这一系列事儿了,它会【禁止类型转换】
explicit Date(int year)
	:_year(year)
{}

在这里插入图片描述

多参构造函数

//多参构造函数
Date(int year, int month ,int day = 31)
	:_year(year)
	,_month(month)
	,_day(day)
{}

  • 根据从右往左缺省的规则,我们在初始化构造的时候要给到2个参数(半缺省参数规定)
  • 这时候我们如果要用类型转换来构造对象,在对多参构造进行初始化的时候在外面加上一个{}就可以了。
Date d2 = { 2023, 3 };

在这里插入图片描述

  • 下面看看一个实用场景
class A
{
public:
	A(int a1, int a2)
		:_a1(a1)
		, _a2(a2)
	{

	}
	A(int a1)
		:_a1(a1)
	{

	}

private:
	int _a1 = 1;
	int _a2 = 2;
};
class Stack
{
public:
	void Push(const A& aa)
	{
		
	}
private:
};

  • 这里我的栈想把A类类型的数据入栈
int main()
{
	Stack st;
	A a1(3);
	st.Push(a1);
	A a2(3, 3);
	st.Push(a2);
	return 0;
}
  • 我们一般都是创建一个A类型对象然后入栈
  • 但是有了隐式类型转换就可以这样写
int main()
{
	Stack st;
	st.Push(3);
	st.Push({ 3,3 });
	return 0;
}

-这里编译器就会自动把3和(3,3)构造一个对象放在临时对象中,然后拷贝构造给push()
在这里插入图片描述
在这里插入图片描述

  • 上面是内置类型到类类型的转换,我们下面看看类类型到类类型的转换。
  • 类类型的对象之间也可以隐式转换,需要相应的构造函数⽀持
class A
{
public:
	// 构造函数explicit就不再⽀持隐式类型转换
	// explicit A(int a1)
	A(int a1)
		:_a1(a1)
	{}
	//explicit A(int a1, int a2)
	A(int a1, int a2)
		:_a1(a1)
		, _a2(a2)
	{}
	void Print()
	{
		cout << _a1 << " " << _a2 << endl;
	}
	int Get() const
	{
		return _a1 + _a2;
	}
private:
	int _a1 = 1;
	int _a2 = 2;
};
class B
{
public:
	B(const A& a)
		:_b(a.Get())
	{}
private:
	int _b = 0;
};

staitc成员

static特性描述

  • 静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。
class A {
public:
	A(int a = 0)
	{
		count++;
	}

	A(const A& a)
	{
		count++;
	}
private:
	static int count;
};
int A::count = 0;
  • 但是静态成员变量是不能在类中声明用缺省值初始化的,因为我们在初始化列表中说过,这里是专门留给各个对象在初始化列表中没初始化的成员变量进行初始化的。而静态成员是属于所有对象的,是共有的

  • 所以我们考虑,把静态成员变量放在类外面初始化。

  • 第二个特性:静态成员变量⼀定要在类外进⾏初始化

  • 可以看到,直接打印访问是不可以的,因为需要域作用限定符::
    在这里插入图片描述

  • 不过呢,加上域作用限定符::又说它是私有的无法访问
    在这里插入图片描述

  • 那么就引出了static的第三条特性👇

  • 静态成员也是类的成员,受public、protected、private 访问限定符的限制

静态成员函数

static int GetCount()
{
	return count;
}

  • 在外界还是使用域作用限定符::便可以访问到,可以说这个【静态成员函数】是专门为静态成员变量而生的,他们都是存在静态区(类的所所有对象都可以访问),但是被类作用域所限定所以我们就把静态成员函数放在public修饰的区域
  • 此时我们就可以获取到静态成员变量的值了
  • 注意:⾮静态的成员函数,可以访问任意的静态成员变量和静态成员函数。静态成员函数只能访问静态成员变量,不能访问非静态成员变量(因为他们没有this指针,刚刚说了静态成员实际是在静态区存,而类是在栈上存)
  • 此时在类中就可以使用域限定符访问成员

面试题实例

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

  • 或许有人立马想到用全局变量来计算构造函数调用的次数。但是我们这里介绍用静态成员的方法。
class C
{
public:
	C()
	{
		++_scout;
	}
	C(const C& t)
	{
		++_scout;
	}
	static int GetCout()
	{
		return _scout;
	}
private:
	static int _scout;
};
int C::_scout = 0;

int main()
{
	C c1;
	C c2;
	C c3 = c2;
	int n = C::GetCout();
	printf("%d", n);
	return 0;
}
  • 这里2个构造和一个拷贝构造就是3个
    在这里插入图片描述

oj挑战

题目描述

在这里插入图片描述

思路解析

  • 这道题不能使用乘除法,就意味着不能使用等差数列公式,不能循环也不能if就不能使用迭代和递归。那么该怎么办呢。
  • 我们上面说的静态成员是不是就可以了,我们不妨创建一个n个数据的类类型数组,此时创建n个类类型的空间就需要调用构造函数,那么我们就使用静态成员变量来计算构造函数调用的次数,再用静态成员函数返回即可。
  • 第一个逻辑就是进行累加,就是把两个静态成员变量_i和_ret放在构造函数中,一调用构造函数_i就自增_ret累加_i,那么就是1+…n的和了。
 Sum()
        {
            _ret += _i;
            _i++;
        }
  • 最后在类的内部提供的一个静态成员函数,外界便可以通过它来获取到静态成员变量sum
static int GetSum()
        {
            return _ret;
        }

整体代码:

class Sum
{
public:
        Sum()
        {
            _ret += _i;
            _i++;
        }
        static int GetSum()
        {
            return _ret;
        }
private:
    static int _i;
    static int _ret;
};
int Sum::_i = 1;
int Sum::_ret = 0;
class Solution {
public:
    int Sum_Solution(int n) {
        Sum a[n];
        return Sum::GetSum();
    }
};

有关static修饰变量注意

  • 设已经有A,B,C,D 4个类的定义,程序中A,B,C,D构造函数调⽤顺序为?()
    设已经有A,B,C,D 4个类的定义,程序中A,B,C,D析构函数调⽤顺序为?()
A:D B A C
B:B A D C
C:C D B A
D:A B D C
E:C A B D
F:C D A B
C c;
int main()
{
A a;
B b;
static D d;
return 0}
  • 先遇到全局域中的c,然后遇到main局部域中的a和b和static修饰的c,局部域按出现的顺序构造,无论是否为static
  • 所以构造为:c a b d,选E
  • 析构按最晚出现的开始构造,注意析构的时候staitc修饰的放在局部域后面(因为其生命周期会延长,但作用域不会发生变化),所以d在ab后面,这样就是b a d c,选B

友元

友元函数

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

  • 特性1:友元函数可访问类的私有和保护成员,但不是类的成员函数(也就是所以得成员都可以访问)

  • 特性2:友元函数不能用const修饰,因为友元函数只是一个全局函数,不属于类的成员函数,所以它没有隐藏的this指针,而const修饰的就是this指针,只有非静态的成员函数才能用const修饰

  • 特性3:友元函数可以在类定义的任何地方声明,不受类访问限定符限制
    在这里插入图片描述

  • 特性4:一个函数可以是多个类的友元函数。(也就是我一个人可以有多个朋友)

  • 比如说一个函数需要访问多个类中的私有成员,那可以在那几个类中设置这个函数为他们的友元函数,这样就都可以访问了

  • 友元函数的调用与普通函数的调用原理相同。直接调用即可,不用像对象的成员函数一样,必须用对象.来访问

友元类

除了友元函数以外,还有一个东西叫做友元类,也就是一个类也可以是另一个类的友元

  • 例如下面有一个Tiime和Date类,在Date类中呢有一个Time类的对象,然后在SetTimeOfDate的这个函数中初始化Time类中的三个成员变量,可是呢_hour、_minite、_second是属于Time类中私有的成员变量,那Date类要如何访问到呢?
  • 此时就可以使用【友元类】了,friend class Date表示在Time类中声明Date类是我的友元类,它可以访问我的私有成员变量
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类中声明了其为它的友元类,但是在Date类中没有声明Time为它的友元类,所以Time是无法访问到Date类中私有成员的
    在这里插入图片描述

  • 是我在Date类中声明Time类为它的友元类,此时访问就不会受限了
    在这里插入图片描述

  • 也可以这么理解,友元类单向就像是舔狗一样,一个男生很喜欢一个女生,但是那个女生呢不喜欢他,所以只好当舔狗🐶(你把我当朋友,但是我不把你当朋友)

  • 友元关系不能传递

  • 这个很简单,比方说如果C是B的友元, B是A的友元,则不能说明C时A的友元

  • 小C是小B的朋友,小A是小B的朋友,那可不能说明小C是小A的朋友哦🙅‍

  • 友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员。

#include<iostream>
using namespace std;
// 前置声明,都则A的友元函数声明编译器不认识B
class B;
class A

{
// 友元声明
friend void func(const A& aa, const B& bb);
private:
	int _a1 = 1;
	int _a2 = 2;
};
class B
{
// 友元声明
	friend void func(const A& aa, const B& bb);
	private:
	int _b1 = 3;
	int _b2 = 4;
};
void func(const A& aa, const B& bb)
{
	cout << aa._a1 << endl;
	cout << bb._b1 << endl;
}
int main()
{
	A aa;
	B bb;
	func(aa, bb);
	return 0;
}
  • 就是func就是A的友元函数也可以是B的友元函数,都可以访问他们的私有成员。

内部类

概念引入

【概念】:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限

  • 看到下面的类A,有一个成员变量_a,其内部还有一个类B,这个类B就叫做内部类
class A
{
private:
	static int k;
	int h;
public:
	class B // B天生就是A的友元
	{
	private:
		int _b;
	public:
		void foo(const A& a)
		{
			cout << k << endl;		//OK
			cout << a.h << endl;	//OK
		}
	};
};

  • 注意:内部类默认是外部类的友元类。也就是说,内部类可以访问外部类的所有成员。可以通过外部类的对象参数
using namespace std;
class A
{
public:
	A(int a1 = 1, int a2 = 2)
		:_a1(a1)
	{
		
	}
	class B
	{
	public:
		void func(const A& a)
		{
			
			_b1 = a._a1;	//静态成员内部类可以直接访问,因为他属于所有对象,所以不用加对象来访问。
							//而这种需要对象来访问的是非静态成员,每一个对象都是有自己的一份。
		}
		int _b1;
	};
private:
	int _a1;
	static int _a2;
};
  • 不过有一点要额外提醒的是因为类B包在类A里面,所以要去实例化类B的对象时会受到类域的限制,此时就可以使用到之前所学的域作用限定符::
    在这里插入图片描述

特性讲解

  • 内部类可以定义在外部类的public、protected、private都是可以的
  • sizeof(外部类)=外部类,和内部类没有任何关系
    在这里插入图片描述
  • 可以把内部类想想成为一个别墅小区中的个人别墅,而外部类就是这个小区。每栋别墅都是别人的私人领地,没有经过允许时不可以私自闯入的。不过小区里面的业主却可以使用小区内的各种公共设施。
  • 对应的就是内部类天生就是外部类的友元,可以访问外部类的私有成员,但是外部类却不可以访问内部类中的成员

oj题优化

  • 对于上面在static中讲的oj题, 就可以优化一下,我们那道题的主要思路就是调用构造函数的次数来计算1+n的和,但是我们知道构造函数没有返回值,所以我们需要在哪里存储每次调用的值并返回,所以用了静态成员变量和静态成员函数。
  • 这里就可以考虑把内部类放在外部类中,这样因为内部类是外部类的友元,所以我们可以直接访问外部类的成员变量,就不需要静态成员函数每次来返回值了。
  • 将两个静态成员变量直接作为Solution类的成员变量,因为Count可以访问到外部类中的私有成员变量,所以不需要加::
  • 而且在最后return返回结果的时候,直接return sum即可,因为这就是Solution自身的成员变量,它当然可以访问,都不需要内部类向外提供静态成员函数
class Solution {
private:
    //作为Solution的内部类
    class Count 
    {
      public:
        Count() 
        {
            sum += i;
            i++;
        }
    };
static int sum;
static int i;
public:
    int Sum_Solution(int n) 
    {
        Count c[n];
        return sum;
    }
};
int Solution::sum = 0;
int Solution::i = 1;

匿名对象

【语法格式】:类名()

class A {
public:
	A(int a)
	{
		cout << "A构造函数的调用" << endl;
		_a = a;
	}

	~A()
	{
		cout << "A析构函数的调用" << endl;
	}
private:
	int _a;
};

  • 首先看到这里有一个类A,然后在下面实例化了两个类A的对象,分别使用的是【有名对象】和【匿名对象】,此时就可以很明显地看出它们的区别之所在,匿名对象很明显就是没有名字
int main(void)
{
	A a1(10);	// 有名对象 -- 生命周期在当前函数局部域
	A(20);		// 匿名对象 -- 生命周期在当前行
	return 0;
}

  • 对于【有名对象】而言,其生命周期在当前函数局部域
  • 对于【匿名对象】而言,其生命周期在当前行

那知道了其特性后我们便可以去用一用这个匿名对象呢

  • 下面有一个类,现在我们要去调用这个类中的成员函数,你会如何去进行调用呢?
class Solution {
public:
	int Sum_Solution(int n) {
		cout << "Sum_Solution" << endl;
		return n;
	}
};

  • 相信最常规的做法就是像下面这样,实例化出一个对象,然后通过对象.函数名()的形式进行调用
Solution s;
s.Sum_Solution(1);

  • 但是呢,我直接使用下面这一种形式也可以做到,即【匿名对象】去进行调用,虽然这一种调用形式比较方便,但是呢是存在局限性的,我们只能调用这么一次,若是你想要多次调用类中的这个函数时,就需要去构造【有名对象】了,其生命周期是到程序结束为止的
Solution().Sum_Solution(2);

  • 学习完基础的概念后我们再来看一些复杂的场景,加深对语法的理解
A& ra = A(1);

  • 这个语法是错的,因为A(1)这里会把1调用构造,构造完的1把他放在临时对象里面,而临时对象这块空间具有常性,如果我们对这块空间引用,那么就会权限放大。
  • 此时我们只需要在前面加上一个const即可
const A& ra = A(1);

  • 那我现在还想问题,这一块也是涉及引用相关的知识,因为这个匿名对象的生命周期只在这一行,那么此时这个ra是否会变成【野引用】呢?即引用了一块已经不存在的空间?
  • 马上,我们来看看结果,可以发现这个匿名对象结束完后这一行并没有像上面那样立即调用【析构函数】,而是在在程序结束之后才去调用,这就是const加长了匿名对象的生命周期,当引用销毁了,匿名对象才会析构。

第二个场景,就是匿名对象初始化缺省参数,我们知道缺省参数要么用常量初始化,要么用全局变量初始化。

A a;
void func1(A aa = a)		//全局对象给缺省
{

}

void func2(A aa = A(1))	//匿名对象给缺省
{

}

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

现代编译器会为了尽可能提⾼程序的效率,在不影响正确性的情况下会尽可能减少⼀些传参和传返
回值的过程中可以省略的拷⻉。

  • 在上面讲explicit关键字的时候,我有提到下面这种写法会引发【隐式类型转换】,而且还画了对应的图示,中间会通过1构造产生一个A类型的临时对象,然后用再去调用拷贝构造完成拷贝。
  • 不过这一块编译器做了一个优化,将【构造 + 拷贝构造】直接转换为【构造】的形式,本模块我们就这一块来进行一个拓展延伸,讲一讲编译器在拷贝对象时期的一些优化
A aa1 = 1;

传值传参

首先来看到的场景是【传值传参】,对象均是使用上面aa1

//传值传参
void func1(A aa)
{
	
}

func1(aa1);
  • 答案是编译器没有进行优化,还是构造加拷贝构造
  • 那如果直接传入一个3呢,会做优化吗?
func1(3);

在这里插入图片描述

  • 可以看到,若是直接传入3的话,就不会去调用拷贝构造了,这个其实和一开始我们讲得那个差不多,把构造 + 拷贝构造优化成了直接构造,【一开始的构造不用理他,为了构造出aa1对象】
  • 接下去我传入一个A(3),会发生什么呢?
func1(A(3));

在这里插入图片描述

  • 通过观察可以发现,和上面那个是一样的,其实读者去演算一下就可以很清楚,A(3)就是一个很明显的有参构造,实例化出一个对象后那就是拷贝构造,但是这里因为编译器的优化,所以直接变成了一个构造

传引用传参

  • 之前的文章里有说过为什么在传参的时候最好使用【传引用传参】,原因就是在于可以减少拷贝,提高程序运行效率
//传引用传参
void func2(const A& aa)		//不加const会造成权限放大
{

}

  • 那通过引用接收aa1会发生什么呢?
  • 通过观察可以发现,无论是【构造】还是【拷贝构造】,都不会去调用,这是为什么呢?
  • 原因就在于这里使用的是引用接收,那么形参部分的aa就是aa1的别名,无需构造产生一个新的对象,也不用去拷贝产生一个,直接用形参部分这个就可以了,现在知道引用传参的好处了吧👈
    在这里插入图片描述

传值返回

//传值返回
A func3()
{
	A aa;
	return aa;	
}

  • 若是直接去调用上面这个func3(),会发生什么呢?
A aa2 = func3();

  • 此处在函数调用的地方我使用一个对象去做了接收,那在上面【构造 + 拷贝构造】的基础上就会再多出一个【拷贝构造】,即为【构造 + 拷贝构造 + 拷贝构造】
  • 不过通过调试可以看出,只进行了一次拷贝构造,这里其实就存在编译器对于【拷贝构造】的一个优化工作,将两个拷贝构造优化成了一个拷贝构造
    在这里插入图片描述

赋值运算符重载不优化

A aa2;
aa2 = func3();	

仔细观察便可以发现,在拷贝构造完成之后又去进行了一次【赋值重载】,那看上面的代码其实就很明显了,那若是一个【拷贝构造】+【赋值重载】的话,编译器其实不会去做一个优化,那这其实相当于干扰了编译器

在这里插入图片描述

传匿名对象返回

还记得上面讲到的【匿名对象】吗,也可以使用它返回哦,效率还不低呢!

//匿名对象返回
A func5()
{
	return A();		//返回一个A的匿名对象
}

  • 先调用一下看看会怎么样
func5();

  • 可以看到本质还是传值返回,照理来说会构造出一个临时对象然后在拷贝构造,但是却没有调用拷贝构造,原因就是匿名对象起到的作用,对于A()你可以就把它看做是一个表达式,一个【构造】+【拷贝构造】就被优化成了直接构造
    在这里插入图片描述
A aa4 = func5();

  • 可以看到,竟然也是只有一个构造。照道理分析来看的话应该是【构造 + 拷贝构造 + 拷贝构造】,不过在匿名对象返回那里已经优化成【直接构造】了,然后再外面的【构造 + 拷贝构造】由引起来编译器的优化,所以最终就只有一个构造了
  • 可以看到,最后我还去调了三次析构函数,第一次就是当然就是aa4,第二次是aa3,第三次便是一开始就有的aa1了,通过这么调试观察,希望你能真正看懂编译器的思维
    在这里插入图片描述

小结

函数传参总结

尽量使用const + &传参,减少拷贝的同时防止权限放大
对象返回总结

接收返回值对象,尽量拷贝构造方式接收,不要赋值接收【会干扰编译器优化】
函数中返回对象时,尽量返回匿名对象【可以增加编译器优化】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值