C++:类和对象4

发布于:2025-05-13 ⋅ 阅读:(84) ⋅ 点赞:(0)

一,日期类实现

学习建议:

对于计算机学习来说,调试十分重要,所以在日常学习中一定要加大代码练习,刷代码题和课后自己敲出课上代码例题,注意不要去对比正确代码或者网上找正确代码直接使用,一定要自己去调试更改。

调试技巧:1,通过不断改变运算对象改变调试范围(学会缩小和放大范围),判断可能出现错误的程序范围

                   2,当不知道问题出现在哪的时候,对于一个对象用监视一步一步走,看该对象的变化,再对比自己的计算,判断出那块代码出现问题

还有很多调试技巧需要自己去理解总结

Date.h

#pragma once

#include<iostream>
using namespace std;
#include<assert.h>

class Date
{
      // 友元函数声明 
      friend ostream& operator<<(ostream& out, const Date& d);
      friend istream& operator>>(istream& in, Date& d);
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
    // 直接定义类⾥⾯,他默认是inline 
    // 只有频繁调⽤的小函数在h文件中定义
    //得到月份所对应的天数
	int GetMonthDay(int year, int month)
	{
		assert(month > 0 && month < 13);

		static int monthDayArray[13] = { -1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

		if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))//闰年的二月份是20天的判断+返回
		{
			// 2
			return 29;
		}
		else
		{
			return monthDayArray[month];
		}
	}
    //d1+=天数
	Date& operator+=(int day);
	Date operator+(int day);
    //d1-=天数
	Date& operator-=(int day);
	Date operator-(int day);

	// ++d1 ->d1.operator++()  前置++
	Date& operator++();

	// d1++ ->d1.operator++(1) 后置++  为了区分,构成重载,给后置++,强⾏增加了⼀个int形参 
    // 这⾥不需要写形参名,因为接收值是多少不重要,也不需要⽤ 
    // 这个参数仅仅是为了跟前置++构成重载区分 
	Date operator++(int);

    //--d1 前置--
	Date& operator--();
    
    //d1-- 后置--
	Date operator--(int);

    //大于 大于等于 小于 小于等于 等于 不等于
	bool operator<(const Date& d);
	bool operator<=(const Date& d);
	bool operator>(const Date& d);
	bool operator>=(const Date& d);
	bool operator==(const Date& d);
	bool operator!=(const Date& d);

	// d1-d2
	int operator-(const Date& d); 

    // 流插⼊ 
    // 不建议,因为Date* this占据了⼀个参数位置,使⽤d<<cout不符合习惯 
    //void operator<<(ostream& out);

    //打印
	void Print();
private:
	int _year;
	int _month;
	int _day;
};

//重载
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);

Date.cpp

#include"Date.h"

bool Date::CheckDate()//检查日期是否正确·
{
     if (_month < 1 || _month > 12|| _day < 1 || _day > GetMonthDay(_year, _month))
     {
       return false;//天数,月数,年数不对时直接报错
     }
     else
     {
       return true;
     }
}

Date::Date(int year, int month, int day)//对日期赋值
{
    _year = year;
    _month = month;
    _day = day;
    if (!CheckDate())
    {
     cout << "⽇期⾮法" << endl;
    }
}

void Date::Print()
{
  cout << _year << "-" << _month << "-" << _day << endl;
}

// d1 < d2
bool Date::operator<(const Date& d) 
{
     if (_year < d._year)//先对年份进行判断
     {
       return true;
     }
     else if (_year == d._year)
     {
       if (_month < d._month)//年份相同再对月份进行判断
       {
         return true;
       }
       else if (_month == d._month)//月份相同时则d1<d2
       {
         return _day < d._day;
       }
     }
     return false;
}

// d1 <= d2
bool Date::operator<=(const Date& d)
{
   return *this < d || *this == d;//直接借用上述的 < 运算符重载
}

//同理
//d1>d2
bool Date::operator>(const Date& d) 
{
  return !(*this <= d);//直接利用上述小于等于运算符取逆
}

//d1>=d2
bool Date::operator>=(const Date& d) 
{
  return !(*this < d);
}

//d1==d2
bool Date::operator==(const Date& d) 
{
  return _year == d._year&& _month == d._month&& _day == d._day;
}

//d1!=d2
bool Date::operator!=(const Date& d) const
{
   return !(*this == d);
}

//d1+=天数
Date& Date::operator+=(int day)//重载运算函数是成员函数,,第一个参数是this指针,参数比运算对象少一个
{
	if (day < 0)// 当要加的数为负数时要求借用减法完成
	{
		return *this -= -day;
	}
	_day += day;//完成加法运算
	while (_day > GetMonthDay(_year, _month))//天数超越了对应月份的天数
	{
		_day -= GetMonthDay(_year, _month);//天数减去对应月份的天数
		++_month;//同时月份跳转到下一个月
		if (_month == 13)//当月份跳转到13时进入下一年,同时月份重新定义为1
		{
			_year++;
			_month = 1;
		}
	}

	return *this;//返回第一个运算对象
}

//d1+天数
Date Date::operator+(int day)//完成加法运算
{
	Date tmp(*this);//拷贝一份*this用拷贝的tmp完成+=运算
	tmp += day;

	return tmp;
}

//d1-=天数
Date& Date::operator-=(int day)//同理,重载运算函数是成员函数
{
	if (day < 0)//减去的数为负数的情况
	{
		return *this += -day;
	}
	_day -= day;//减法运算
	while (_day <= 0)//天数小于等于0
	{
		--_month;//月份-1
		if (_month = 0)//当月份为0时,跳转到上一年,同时月份重新定义为12
		{
			_year--;
			_month = 12;
		}
		_day += GetMonthDay(_year, _month); //  不断借助上一个月天数,使得负_day绝对值变小,直至不满足循环条件 
	}
	return *this;//返回自身
}

//d1-天数
Date Date::operator-(int day)
{
	Date tmp(*this);//自身不能改变,拷贝,借助拷贝对象改变返回
	tmp -= day;
	return tmp;
}

//++d1
Date& Date::operator++()
{
	*this += 1;
	return *this;
}

//d1++
Date Date::operator++(int)
{
	Date tmp(*this);
	*this += 1;

	return tmp;
}

//--d1
Date& Date::operator--()
{
	*this -= 1;
	return *this;
}

//d1--
Date Date::operator--(int)
{
	Date tmp(*this);
	*this -= 1;
	return tmp;
}

//d1-d2
int Date::operator-(const Date& d) 
{
   int flag = 1;//假设法
   Date max = *this;//假设第一个大,第二个小
   Date min = d;
   if (*this < d)//假设错了就交换
   {
     max = d;
     min = *this;
     flag = -1;
   }
     int n = 0;
   while (min != max)
   {
     ++min;
     ++n;//借助n计算相差个数
   }
   return n * flag;//flag就是在两个数大小不同情况下确定运算是正还是负
}

ostream& operator<<(ostream& out, const Date& d)
{
   out << d._year << "年" << d._month << "⽉" << d._day << "⽇" << endl;
   return out;
}


istream& operator>>(istream& in, Date& d)
{
   cout << "请依次输⼊年⽉⽇:>";
   in >> d._year >> d._month >> d._day;
   if (!d.CheckDate())
   {
     cout << "⽇期⾮法" << endl;
   }
   return in;
}






d1-d2的实现思路:

思路一:

1,小的月和天,朝大的对齐,算出来差了多少天

2,年直接减,算出来差多少天

例子:2025.3.11  和2020.5.1

先将3.11向5.1对齐发现三月十一到五月一号差了17+30+1天,此时为2025.5.1和2020.5.1相差五年再看其中闰年,从而计算相差天数再减去48天

思路二:直接赋用

将小的日期不断++直到与大的一致,算出来++了多少次。

上述代码就是借助思路二实现的

Test.cpp

#include"Date.h"
void TestDate1()
{
 // 这⾥需要测试⼀下⼤的数据+和- 
   Date d1(2024, 4, 14);
   Date d2 = d1 + 30000;
   d1.Print();
   d2.Print();

   Date d3(2024, 4, 14);
   Date d4 = d3 - 5000;
   d3.Print();
   d4.Print();

   Date d5(2024, 4, 14);
   d5 += -5000;
   d5.Print();
}

void TestDate2()
{
   Date d1(2024, 4, 14);
   Date d2 = ++d1;
   d1.Print();
   d2.Print();

   Date d3 = d1++;
   d1.Print();
   d3.Print();

   d1.operator++(1);
   d1.operator++(100);
   d1.operator++(0);
   d1.Print();
}

void TestDate3()
{
   Date d1(2024, 4, 14);
   Date d2(2034, 4, 14);
   int n = d1 - d2;
   cout << n << endl;
   n = d2 - d1;
}

void TestDate4()
{
   Date d1(2024, 4, 14);
   Date d2 = d1 + 30000;
   // operator<<(cout, d1)
      cout << d1;
      cout << d2;

      cin >> d1 >> d2;
      cout << d1 << d2;
}
void TestDate5()
{
    const Date d1(2024, 4, 14);
    d1.Print();

    //d1 += 100;
    d1 + 100;

    Date d2(2024, 4, 25);
    d2.Print();

    d2 += 100;

    d1 < d2;
    d2 < d1;
}

int main()
{
   return 0;
}


二,取地址运算符重载

1,const成员函数

(1)将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后 ⾯。

(2)const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进⾏修改。 const修饰Date类的Print成员函数,Print隐含的this指针由 Date* const this 变为 const Date* const this

在上述的成员函数后面都可以加上const。

下面例子出现权限放大:调用print函数需要传递this指针,也就是&d1,而this指针这里相当于Date const this

而上面的const修饰的相当于const Date*,传递过去时会发生权限放大,报错

例子2:

const Date*传递过去会发生权限的平移

Date*传递过去发生权限缩小

2,取地址运算符重载

取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,⼀般这两个函数编译器⾃动 ⽣成的就可以够我们⽤了,不需要去显⽰实现。除⾮⼀些很特殊的场景,⽐如我们不想让别⼈取到当前类对象的地址,就可以⾃⼰实现⼀份,胡乱返回⼀个地址。

class Date
{ 
public :
 Date* operator&()
 {
   return this;
   // return nullptr;//当不想让别人取到地址,可以返回nullptr或者return (Date*)0x112245EF 返回一个假地址
 }
 
 const Date* operator&()const
 {
   return this;
   // return nullptr;
 }
private :
   int _year ; // 年 
   int _month ; // ⽉ 
   int _day ; // ⽇ 
};

三,再探构造函数

(1)之前我们实现构造函数时,初始化成员变量主要使⽤函数体内赋值,构造函数初始化还有⼀种⽅ 式,就是初始化列表,初始化列表的使⽤⽅式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成 员列表,每个"成员变量"后⾯跟⼀个放在括号中的初始值或表达式

class Date
{
public:
    //初始化列表
	Date(int& x, int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _n(10)
		,_ref(x)
//可以混合使用两者初始化方法
	{
		_day = day;
        _n=1;//不可行,初始化列表的变量只能初始化一次
        
	}

private:
	int _year;
	int _month;
	int _day;

	// 必须在初始化列表初始化的成员
	const int _n;//常量初始化
	int& _ref;//引用初始化

};


(2)每个成员变量在初始化列表中只能出现⼀次,语法理解上初始化列表可以认为是每个成员变量定义 初始化的地⽅。

(3)引⽤成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进⾏初始 化,否则会编译报错。

class Date
{
public:
 Date(int& x, int year = 1, int month = 1, int day = 1)
 :_year(year)
 ,_month(month)
 ,_day(day)
 ,_t(12)
 ,_ref(x)
 ,_n(1)
 {
 // error C2512: “Time”: 没有合适的默认构造函数可⽤ 
 // error C2530 : “Date::_ref” : 必须初始化引⽤ 
 // error C2789 : “Date::_n” : 必须初始化常量限定类型的对象 
 }
 void Print() const
{
 cout << _year << "-" << _month << "-" << _day << endl;
 }
private:
 int _year;
 int _month;
 int _day;
 Time _t; // 没有默认构造 
 int& _ref; // 引⽤ 
 const int _n; // const 
};

(4)C++11⽀持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显⽰在初始化列表初始化的 成员使⽤的。

class Time
{
public:
	Time(int hour)
		:_hour(hour)
	{
		cout << "Time()" << endl;
	}
private:
	int _hour;
};

class Date
{
public:
	Date()
		:_month(2)
	{
		cout << "Date()" << endl;
	}

	void Print() const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	// 注意这里不是初始化,这里给的是缺省值,这个缺省值是给初始化列表的
	// 如果初始化列表没有显示初始化,默认就会用这个缺省值初始化
	int _year = 1;//year没有在列表中初始化,但是通过了缺省值进行初始化。
	int _month = 1;
	int _day;

	const int _n = 1;
	Time _t = 10;
	int* _ptr = (int*)malloc(12);//缺省值也可以是一个表达式,其实只要是一个值就行
};

class Stack
{
public:
	Stack(int n = 4)
	{
		cout << "Stack(int n)" << endl;
	}
private:
	//...
};

class MyQueue
{
public:
	MyQueue(int n)
		;_pushst(n)
		,popost(n)
	{}

private:
	// 声明
	Stack _pushst;//自定义类型没有默认构造,需要显示的写构造函数
	Stack _popst;
	const int _n = 1; // 缺省值初始化
};
int main()
{
	Date d1;
	MyQueue q;//调用初始化构造函数

	return 0;
}

(5)尽量使⽤初始化列表初始化,因为那些你不在初始化列表初始化的成员也会⾛初始化列表,如果这 个成员在声明位置给了缺省值,初始化列表会⽤这个缺省值初始化。如果你没有给缺省值,对于没 有显⽰在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。对于没有 显⽰在初始化列表初始化的⾃定义类型成员会调⽤这个成员类型的默认构造函数,如果没有默认构 造会编译错误。

(6)初始化列表中按照成员变量在类中声明顺序进⾏初始化,跟成员在初始化列表出现的的先后顺序⽆ 关。建议声明顺序和初始化列表顺序保持⼀致。

初始化列表:

⽆论是否显⽰写初始化列表,每个构造函数都有初始化列表;

⽆论是否在初始化列表显⽰初始化成员变量,每个成员变量都要⾛初始化列表初始化;

练习题:

#include<iostream>
using namespace std;
class A
{
  public:
        A(int a)
          :_a1(a)
          , _a2(_a1)//初始化列表中按照成员变量在类中声明顺序进⾏初始化,跟成员在初始化列表出现的的先后顺序⽆关
        {}
       void Print()
       {
        cout << _a1 << " " << _a2 << endl;
       }
 private:
        int _a2 = 2;//列表中已经进行了显式的初始化,所以这里与缺省值无关了
        int _a1 = 2;
};

int main()
{
   A aa(1);
   aa.Print();
}

先用a1初始化a2,但a1未进行初始化,所以a1是一个随机值,a2就是一个随机值。

再用a=1初始化a1,a1就是1

上述程序的运行结果是:输出1和随机值

四,类型转换

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

不同编译器是否支持优化是不一样的。

(2)构造函数前⾯加explicit就不再⽀持隐式类型转换。

(3)类类型的对象之间也可以隐式转换,需要相应的构造函数⽀持。

class A
{
public:
	// 构造函数explicit就不再支持隐式类型转换
	// explicit A(int a1)
	A(int a1)
		:_a1(a1)
	{
		cout << "A(int a1)" << endl;
	}

	A(const A& aa)
	{
		cout << "A(const A& aa)" << endl;
	}

	//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;
};

class Stack
{
public:
	void Push(const A& aa)//临时对象有常性
	{}

	void Push(const B& bb)
	{}
};


int main()
{
	// 构造
	A aa1(1);

     // 隐式类型转换
	// 1构造临时对象,临时对象拷贝构造aa2,优化后直接构造
    // 1构造⼀个A的临时对象,再⽤这个临时对象拷⻉构造aa3 
    // 编译器遇到连续构造+拷⻉构造->优化为直接构造 
	A aa2 = 1;
    
	A& aa3 = aa2;//前面加上一个const时为普通引用,不可行,因为临时对象具有常性
	const A& aa4 = 1;

	Stack st;
	A aa10(10);
	st.Push(aa10);
    //原来需要定义一个aa10对象,拷贝构造给Push函数,再进行push操作
    //而类型转换可以直接创建临时变量进行拷贝构造,进行优化,传参
	st.Push(10); // 隐式类型转换支持这样的间接传参

	
    // C++11之后才⽀持多参数转化 
    A aa3 = { 2,2 };
	A aa5(1, 1);
	// 隐式类型转换
	// { 1, 1 }构造临时对象,临时对象拷贝构造aa5,优化后直接构造
	A aa6 = { 1, 1 };

	A aa11(11, 11);
	st.Push(aa11);
	st.Push({ 11, 11 }); // 隐式类型转换支持这样的间接传参

	B b1(aa1);//自定义类型转换为自定义类型
	// 隐式类型转换
	B b1 = aa1;

	B b3(aa1);
	st.Push(b3);
	st.Push(aa1);

	return 0;
}

当优化关闭后,会出现构造+拷贝构造

五,static成员

static:

(1)⽤static修饰的成员变量,称之为静态成员变量,静态成员变量⼀定要在类外进⾏初始化

(2)静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区,生命周期是全局的。因此对象大小不包括静态成员

(3)⽤static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针

(4)静态成员函数中可以访问其他的静态成员,但是不能访问⾮静态的,因为没有this指针

(5)⾮静态的成员函数,可以访问任意的静态成员变量和静态成员函数

(6)突破类域就可以访问静态成员,可以通过类名::静态成员或者对象.静态成员来访问静态成员变量 和静态成员函数

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

(8)静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员 变量不属于某个对象,不⾛构造函数初始化列表。

声明:

private:
 // 类⾥⾯声明 
 static int _scount;

初始化:

// 类外⾯初始化 
int A::_scount = 0;

计算程序中创建了多少个类的对象

通过count看调用了多少次构造函数

静态成员变量不走初始化列表,不能给缺省值

通过GetACount公有的成员函数对私有的静态成员变量进行访问。

静态成员变量是公有时的访问方式

1,静态成员变量属于所有对象,可以通过指定对象来访问。

count<<a1._scount<<endl;

2,可以指定类域进行访问。

count<<A::_scount<<endl;

注意:静态成员函数无法对非静态成员进行访问

原因:(1)静态成员没有this指针

(2)而非静态成员可以通过类域访问

题目1:

思路:通过Solution成员函数构建对象,从而去调用Sum类域中的构造函数,构造对象大小有多大就调用多少次,并且是静态成员变量,所以全局调用,每次使用的ret和i都是上次变化后的,来实现求和。

一般刷题网站都是支持变长数组的,但VS是不支持的

VS中更改:

Sum*ptr=new Sum[n];
delete[]ptr;//动态开辟
#include<iostream>
using namespace std;
class A
{
public:
	A()
	{
		++_scount;
	}
private:
	//.......
};

int main()
{
	A a1, a2;
	A a3(a1);//定义一个对象调用一次构造
    A aa1[10];//一次性定义多个对象,调用多次构造函数
	return 0;
}

题目2:

构造和析构顺序

构造:全局变量在main函数之前创建,调用其构造函数,C最先调用,而main函数中静态成员变量只要在使用时才会构造,所以先调用A,B的构造。同样,对于多次调用有静态变量函数只会调用一次构造函数,因为只会初始化一次。

析构:main函数栈帧先销毁,先销毁局部变量,后定义的先析构,对于全局变量(可以通过监视地址,进入析构函数监视this指针观察)发现静态成员变量先析构。

六,友元

1,流插入和提取的运算符重载

void TestDate6()
{
   Date d1(2025,3,12);
   printf("%");
}

对于printf和scanf只能打印内置类型,而对于自定义类型无法进行打印,C++为了解决这个问题,设计了流插入和流提取。

void TestDate6()
{
	int i = 22;
	double d = 1.2;
	cout << i;
	cout.operator<<(i);//运算符重载 相当于调用运算符重载调用函数,自动识别类型是因为重载机制,自动调用对应类型的重载

	cout << endl;

	cout << d;
	cout.operator<<(d);

	cout << i << " " << d << endl;*/
}

这里下图更能直观解释每次重载会调用其对于类型的重载函数。

但是这种方法效率会降低,可以加上以下三行代码提高效率。

ios_base::sync_with_stdio(false);
 cin.tie(nullptr);
 cout.tie(nullptr);

流插入ostream函数:

如:

ostream& operator<<(ostream& out)
	{
		out << _year << "年" << _month << "月" << _day << "日" << endl;
		return out;
	}

但是对应的会出现错误

void TestDate6()
{
    //cout<<d1此时会报错,因为左右参数配不上
	//d1 << cout;也会有一点小错误
}

参数配不上的图解:这里out就是cout的别名

所以上述ostream函数有错误,不能重载为成员函数,否则参数会对不上。

//cout<<d1参数对应上了
ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
	return out;
}

这里问题就是访问私有变量:

解决方法:(1)提供一个get成员函数  (2)声明为友元函数

......
//友元声明  可以放到任意位置,一般放到类开始
friend ostream& operator<<(ostream& out, const Date& d);
private:
	int _year;
	int _month;
	int _day;
};

但是此时友元为全局函数,会在多个文件中包含,符号表就会重复,需要声明和定义分离。

解决方法:(1)h中声明,cpp中定义  (2)在函数前加上inline

流提取函数:

istream& operator>>(istream& in, Date& d);

不加const,因为提取的值要放到该对象中,所以加引用。

友元声明:

friend istream& operator>>(istream& in, Date& d);
inline istream& operator>>(istream& in, Date& d)
{
	while (1)
	{
		cout << "请依次输入年月日:>";
		in >> d._year >> d._month >> d._day;
		if (!d.CheckDate())
		{
			cout << "输入日期非法,请重新输入!!!" << endl;
		}
		else
		{
			break;
		}
	}

	return in;
}

void TestDate6()
{
	Date d1(2025, 3, 0);
	Date d2;
	cin >> d1 >> d2;//连续输入输出
	cout << d1 << d2;
}

2友元

(1)友元提供了⼀种突破类访问限定符封装的⽅式,友元分为:友元函数和友元类,在函数声明或者类 声明的前⾯加friend,并且把友元声明放到⼀个类的⾥⾯

(2)外部友元函数可访问类的私有和保护成员,友元函数仅仅是⼀种声明,他不是类的成员函数

(3)友元函数可以在类定义的任何地⽅声明,不受类访问限定符限制

(4)⼀个函数可以是多个类的友元函数

void func(const A& aa, const B& bb)
{
 cout << aa._a1 << endl;
 cout << bb._b1 << endl;
}
//可以把这个函数既放到类域A中又放到类域B中

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

// 前置声明,否则A的友元函数声明编译器不认识B 
class B;
class A
{
  // 友元声明 
  friend void func(const A& aa, const B& bb);
private:
  int _a1 = 1;
  int _a2 = 2;
};
class A
{
 // 友元声明 
  friend class B;
private:
 int _a1 = 1;
 int _a2 = 2;
};
class B
{
public:
  void func1(const A& aa)
  {
    cout << aa._a1 << endl;
    cout << _b1 << endl;
  }
  void func2(const A& aa)
  {
    cout << aa._a2 << endl;
    cout << _b2 << endl;
  }
private:
 int _b1 = 3;
 int _b2 = 4;
};

(6)友元类的关系是单向的,不具有交换性,⽐如A类是B类的友元,但是B类不是A类的友元。

(7)友元类关系不能传递,如果A是B的友元,B是C的友元,但是A不是C的友元。

(8)有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多⽤。

七,内部类

(1)如果⼀个类定义在另⼀个类的内部,这个内部类就叫做内部类。内部类是⼀个独⽴的类,跟定义在 全局相⽐,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。

(2)内部类默认是外部类的友元类。

(3)内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使⽤,那么可以考 虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其 他地⽅都⽤不了。

class A
{
private:
   static int _k;
   int _h = 1;
public:
   class B // B默认就是A的友元 
   {
     public:
     void foo(const A& a)
     {
       cout << _k << endl; //OK
       cout << a._h << endl; //OK
     }
 
     int _b1;
   };
};

int A::_k = 1;

int main()
{
  cout << sizeof(A) << endl;
  //B:: _b1; 是访问不到的,受到A类域的限制
  A::B b;
  A aa;
  b.foo(aa);
  return 0;
}

题目1:

求1+2+3+...+n_牛客题霸_牛客网

class Solution 
{
  // 内部类 
  class Sum
  {
    public:
         Sum()
         {
           _ret += _i;
           ++_i;
          }
  };
  static int _i;
  static int _ret;
  public:
        int Sum_Solution(int n)
 {
        // 变⻓数组 
        Sum arr[n];
        return _ret;
 }
};
int Solution::_i = 1;
int Solution::_ret = 0;

图解:

八,匿名对象

临时对象:(1)类型转换时会产生   (2)传值返回会产生

1,⽤类型(实参)定义出来的对象叫做匿名对象,相⽐之前我们定义的类型对象名(实参)定义出来的 叫有名对象。

2,匿名对象⽣命周期只在当前⼀⾏,⼀般临时定义⼀个对象当前⽤⼀下即可,就可以定义匿名对象。

int main()
{
	// 有名对象
	A aa1(1);

	// 转换产生的临时对象  const引用可以延长其生命周期
	const A& aa2 = 1;
    // 但是他的⽣命周期只有这⼀⾏,我们可以看到下⼀⾏他就会⾃动调⽤析构函数 

	// 匿名对象
	A(2);
    
    A aa1;
    // 不能这么定义对象,因为编译器⽆法识别下⾯是⼀个函数声明,还是对象定义 

    //A aa1();
    // 但是我们可以这么定义匿名对象,匿名对象的特点不⽤取名字, 
    
    

    return 0;
}
class Solution 
{
 public:
   int Sum_Solution(int n)
 {
   //...
   return n;
 }
};
int main()
{
  //有名调用
  Solution s;
  s.Sum_Solution(10);

 //匿名调用
  Solution().Sum_Solution(10);
}

九,对象拷贝时的编译器优化

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

(2)如何优化C++标准并没有严格规定,各个编译器会根据情况⾃⾏处理。当前主流的相对新⼀点的编 译器对于连续⼀个表达式步骤中的连续拷⻉会进⾏合并优化,有些更新更"激进"的编译器还会进⾏ 跨⾏跨表达式的合并优化。

(3)linux下可以将下⾯代码拷⻉到test.cpp⽂件,编译时⽤ g++ test.cpp -fno-elide-constructors 的⽅式关闭构造相关的优化。

int main()
{
	// 构造临时对象,临时对象再拷贝构造aa1,优化为直接构造
	//A aa1 = 1;

	// 传值传参
	// 无优化
	 A aa1;
	//f1(aa1); 已经创建出对象,再去拷贝构造是没有优化的
     //cout << endl;
	
	 //下面两者情况是有优化的
	 隐式类型,连续构造+拷贝构造->优化为直接构造
	f1(1);
	 一个表达式中,连续构造+拷贝构造->优化为一个构造
	f1(A(2));
	//cout << endl;
}

图解观察:

传值返回:先构造,再创建临时对象,返回临时对象,再调用拷贝构造。

A f2()
{
	A aa;
	return aa;
}

int main()
{
  // 传值返回
  // 返回时一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造 (vs2019 debug)
  // 一些编译器会优化得更厉害,进行跨行合并优化,直接变为构造。(vs2022 debug)
  f2();
  cout << endl;
  // 返回时一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造 (vs2019 debug)
  // 一些编译器会优化得更厉害,进行跨行合并优化,直接变为构造。(vs2022 debug)
  A aa2 = f2();
  cout << endl;
  
  // 接收返回值时
  A ret=f2();
  cout<<endl;
}

未接收返回值时图解:左侧关闭优化,右侧开启

接收返回值时图解:构造+拷贝构造+析构,返回值给ret时又要拷贝构造+析构。

拷贝构造+赋值重载:

未优化:构造+拷贝构造+析构+赋值重载+析构

int main()
{
  // 一个表达式中,连续拷贝构造+赋值重载->无法优化
  aa1 = f2();
  cout << endl;

  return 0;
}

图解:

总结:


网站公告

今日签到

点亮在社区的每一天
去签到