【C++】类和对象(下篇)(万字)

时间:2022-09-26 10:35:34

????C++学习历程:入门


  • 博客主页:一起去看日落吗
  • 持续分享博主的C++学习历程
  • 博主的能力有限,出现错误希望大家不吝赐教
  • 分享给大家一句我很喜欢的话: 也许你现在做的事情,暂时看不到成果,但不要忘记,树????成长之前也要扎根,也要在漫长的时光????中沉淀养分。静下来想一想,哪有这么多的天赋异禀,那些让你羡慕的优秀的人也都曾默默地翻山越岭????。

【C++】类和对象(下篇)(万字)

这篇文章是对类和对象的一个收尾和补充⭐️ ???? ????


????1. 再谈构造函数

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

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

class A
{
public:
	A(int a = 0)
	{
		_a = a;	
		cout << "A(int a = 0)" << endl;
	}
	A& operator=(const A& aa)//不写也行,因为这里只有内置类型,默认生成的就可以完成
	{
		cout << "A& operator=(const A& aa)" << endl;
		if(this != &aa)
		{
			_a = aa._a;
		}
		return *this;
	}
private:
	int _a;
};
class B
{
public:
	B(int a, int b)
	{
		//_aa._a = a;//err:无法访问private成员
		
		/*A aa(a);
		_aa = aa;*/ 
		_aa = A(a);//简化版,同上
		
		_b = b;
	}
private:
	int _b = 1;
	A _aa;
};
int main()
{
	B b(10, 20);
	return 0;
}

【C++】类和对象(下篇)(万字)


????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成员变量
  • 自定义类型成员(且该类没有默认构造函数时)
class A
{
public:
	A(int a)
		:_a(a)
	{}
private:
	int _a;
};
class B
{
public:
	B(int a, int ref)
		:_aobj(a)
		,_ref(ref)
		,_n(10)
	{}
private:
	A _aobj; // 没有默认构造函数
	int& _ref; // 引用
	const int _n; // const
};
  1. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
class Time
{
public:
Time(int hour = 0)
:_hour(hour)
{
cout << "Time()" << endl;
}
private:
	int _hour;
};
class Date
{
public:
	Date(int day)
	{}
private:
	int _day;
	Time _t;
};
int main()
{
	Date d(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();
}
A. 输出1 1
B.程序崩溃
C.编译不通过
D.输出1 随机值(Yes)

如上程序的输出结果是 D 选项,因为 C++ 规定成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其初始化列表中出现的先后次序无关。实际中,建议声明顺序和初始化列表顺序保持一致,避免出现这样的问题。


????1.3 explicit关键字

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

class Date
{
public:
	// 1. 单参构造函数,没有使用explicit修饰,具有类型转换作用
	// explicit修饰构造函数,禁止类型转换---explicit去掉之后,代码可以通过编译
	explicit Date(int year)
	:_year(year)
	{}
/*
// 2. 虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具有类型转
换作用
// explicit修饰构造函数,禁止类型转换
explicit Date(int year, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
*/
	Date& operator=(const Date& d)
	{
	if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
	return *this;
	}
private:
	int _year;
	int _month;
	int _day;
};
void Test()
{
Date d1(2022);
// 用一个整形变量给日期类型对象赋值
// 实际编译器背后会用2023构造一个无名对象,最后用无名对象给d1对象进行赋值
d1 = 2023;
// 将1屏蔽掉,2放开时则编译失败,因为explicit修饰构造函数,禁止了单参构造函数类型转换的作用
}

上述代码可读性不是很好,用explicit修饰构造函数,将会禁止构造函数的隐式转换。


????2. static成员

????2.1 概念

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

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

int countC = 0;
int countCC = 0;
class A
{
public:
	A()
	{
		++countC;
	}
	A(const A& a)
	{
		++countCC;
	}
};
A f(A a)
{
	A ret(a);
	return ret;
}
int main()
{
	A a1 = f(A());
	A a2;
	A a3;
	a3 = f(a2);
	cout << countC << endl;
	cout << countCC << endl;
	return 0;
}

这样虽然能计算出结果,但是有一个问题,countC 和 countCC 是可以随便改的,这样就很不好。

class A
{
public:
	A()
	{
		++_count;
	}
	A(const A& a)
	{
		++_count;
	}
	int GetCount()
	{
		return _count;	
	}
	static int GetCount()
	{
		return _count;	
	}
private:
	int _a;
	static int _count;
};
//定义初始化
int A::_count = 0;
A f(A a)
{
	A ret(a);
	return ret;
}
int main()
{
	A a1 = f(A());
	A a2;
	A a3;
	a3 = f(a2);
	cout << sizeof(A) << endl;
	
	//这里就体现了static成员属于整个类,也属于每个定义出来的对象共享,但限制于公有
	/*cout << A::_count << endl;	
	cout << a1._count << endl;
	cout << a2._count << endl;*/

	/*A ret;
	cout << ret.GetCount() - 1 << endl;*/
	/*cout << A().GetCount() - 1 << endl;*/
	cout << A::GetCount() << endl;
	
	return 0;
}

static int _count; 存在静态区,属于整个类,也属于每个定义出来的对象共享。对于非 static 成员它们的定义是在初始化列表中,但在 C++ 中,static 静态成员变量是不能在类的内部定义初始化的,这里的内部只是声明。定义一个公有函数 GetCount 函数,返回 _count:_count是私有,访问它。


????2.2 特性

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

【问题】

  1. 静态成员函数可以调用非静态成员函数吗?
    答:不能,因为静态成员函数没有 this 指针。
  2. 非静态成员函数可以调用类的静态成员函数吗?
    答:可以,因为非静态成员函数有 this 指针。

????3. 友元

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

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

????3.1 友元函数

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

class Date
{
public:
	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
		{}
	// d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
	// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
	ostream& operator<<(ostream& _cout)
	{
		_cout << _year << "-" << _month << "-" << _day << endl;
		return _cout;
	}
private:
	int _year;
	int _month;
	int _day;
};

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

class Date
{
	friend ostream& operator<<(ostream& _cout, const Date& d);
	friend istream& operator>>(istream& _cin, Date& d);
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};
	ostream& operator<<(ostream& _cout, const Date& d)
	{
		_cout << d._year << "-" << d._month << "-" << d._day;
		return _cout;
	}
	istream& operator>>(istream& _cin, Date& d)
	{
		_cin >> d._year;
		_cin >> d._month;
		_cin >> d._day;
		return _cin;
	}
int main()
{
	Date d;
	cin >> d;
	cout << d << endl;
	return 0;
}

说明:

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

????3.2 友元类

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

  • 友元关系是单向的,不具有交换性。
  • 比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time
  • 类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
  • 友元关系不能传递
    如果B是A的友元,C是B的友元,则不能说明C时A的友元。
  • 友元关系不能继承,在继承位置再给大家详细介绍。
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;
};

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

1、友元关系是单向的,不具有交换性。比如上述 Time 类和 Date 类,在 Time 类中声明 Date 类为其友元类,那么可以在 Date 类中直接访问 Time 类的私有成员变量,但想在 Time 类中访问 Date 类中私有的成员变量则不行。
2、友元关系不能传递,如果 C 是 B 的友元, B 是 A 的友元,则不能说明 C 是 A 的友元。


????4. 内部类

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

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

特性:

  1. 内部类可以定义在外部类的public、protected、private都是可以的。
  2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
  3. sizeof(外部类)=外部类,和内部类没有任何关系。
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	
		}
	private:
		int _b;
	};
};
int A::k = 0;
int main()
{
	cout << sizeof(A) << endl;//4
	A::B b;//要用B去定义,必须得指定域
	b.foo(A());
	return 0;
}


sizeof 在计算 A 类型对象大小的时候,不考虑 B 类。因为 B 作为 A 的内部类,跟普通类没有什么区别,只是定义在 A 的内部,它受到 A 的类域的限制和访问限定符的限制。


????5. 再次理解类和对象

现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创建对象后计算机才可以认识。比如想要让计算机认识洗衣机,就需要:

1. 用户先要对现实中洗衣机实体进行抽象—即在人为思想层面对洗衣机进行认识,洗衣机有什么属性,有那些功能,即对洗衣机进行抽象认知的一个过程

2. 经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言(比如:C++、Java、Python等)将洗衣机用类来进行描述,并输入到计算机中

3. 经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才能洗衣机是什么东西。

4. 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。

在类和对象阶段,大家一定要体会到,类是对某一类实体(对象)来进行描述的,描述该对象具有那些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象。

【C++】类和对象(下篇)(万字)


⭐️拓展:C++11的成员初始化新玩法

class A
{
public:
	A(int a = 0)
		: _a(0)
	{}
private:
	int _a;
};
class B
{
private:
	//缺省值 
	int _b = 0;
	int* p = (int*)malloc(sizeof(int)*10);
	A _aa = A(10);//先构造再拷贝构造,优化为构造
	A _aa = 10;//同上,建议
	//static int _n = 10;//err,静态变量不能给缺省值
};
int main()
{
	B bb;	
	return 0;
}

C++11 支持非静态成员变量在声明时进行初始化赋值,但是要注意这里不是初始化,这里是给声明的成员变量缺省值 —— 如果在构造函数中显示给值就会不用缺省值,如果没有显示给,就会用缺省值


⭐️练习题

????求1+2+3+…+n

✨练习原题

【C++】类和对象(下篇)(万字)

【C++】类和对象(下篇)(万字)

class Sum {
  public:
    Sum() {
        _ret += _i;
        _i++;
    }
    static int GetRet() { //在保证封装的情况下访问_ret
        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];//调用n次构造
        return Sum::GetRet();
    }
};

????计算日期到天数转换

✨练习原题

【C++】类和对象(下篇)(万字)

核心思想:之前我们是把一年中每月的天数存储起来,这里我们存储的是包括当前月份之前的天数。对应的月份 + 天数就是当前默认的天数,再判断闰年。

#include<iostream>
using namespace std;
int main() {
    //static int monthDays[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31,30,31};
    //改造:
    static int monthDays[13] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365};
    //2021 10 18
    int year, month, day;
    cin >> year >> month >> day;
    int n = monthDays[month - 1] + day;
    //判断2月,注意这里不是等于而是大于
    if (month > 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)) {
        n++;
    }
    cout << n << endl;
    return 0;
}

????日期差值

✨练习原题

【C++】类和对象(下篇)(万字)

题解:本题将日期差值分为三段计算

  • 第一段是小年度过的时间
  • 第二段是大年度过的时间
  • 最后一段是,包括小年在内的,小年到大年之前的各年时间之和
  • 3段-1段+2段+1即可
#include <iostream>
#include <cmath>
#include <cstdlib>
using namespace std;

static const int month[2][13] = {
    {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
    {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
};
static const int year[2] = {365, 366};

int main() {
    int isRun = 0;
    int Y1, M1, D1, Y2, M2, D2;
    while (scanf("%4d%2d%2d", &Y1, &M1, &D1) != EOF) {
        scanf("%4d%2d%2d", &Y2, &M2, &D2);
        int PastDays1 = 0, PastDays2 = 0, YearGapDays = 0;
        //此处需要求出各日期,在本年内过了多久
        //以及相差的年份中过了多久

        if ((!Y1 % 4 && Y1 % 100) || !Y1 % 400)
            isRun = 1;
        else
            isRun = 0;
        for (int i = 0; i < M1; i++)
            PastDays1 += month[isRun][i];
        PastDays1 += D1;
        //以上求出了第一个日期在本年内过了多久

        if ((!Y2 % 4 && Y2 % 100) || !Y2 % 400)
            isRun = 1;
        else
            isRun = 0;
        for (int i = 0; i < M2; i++)
            PastDays2 += month[isRun][i];
        PastDays2 += D2;
        //此处求出了第二个日期在本年内过了多久

        for (int i = min(Y1, Y2); i < max(Y1, Y2); i++) {
            if ((!i % 4 && i % 100) || !i % 400)
                isRun = 1;
            else
                isRun = 0;
            YearGapDays += year[isRun];
        } //此处求出了算上最小年在内,年之间差了多久

        if (Y1 > Y2)
            cout << YearGapDays + PastDays1 - PastDays2 + 1;
        else
            cout << YearGapDays + PastDays2 - PastDays1 + 1;
        //此处给出结果,需要区分哪个是大年
        //需要减去小年度过的天数
        //并加上大年度过的天数
    }
    return 0;
}

????打印日期

✨练习原题

【C++】类和对象(下篇)(万字)

用C++类的构造函数自动完成

#include <iostream>
#include <cstdio>
using namespace std;

int main() {
    int year;
    int k;
    int number[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

    while (scanf("%d %d", &year, &k) != EOF) {
        int mouth = 1;  //初始月份为1
        int date = 0;   //初始日为0

        if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) {
            number[1] = 29;
        } else {
            number[1] = 28;
        }  //判断是否闰年,闰年2月赋值为29天

        k = k - number[mouth - 1]; //减掉当前月份的天数
        while (k >
                0) { //若大于零,则表示月份可能可以继续加,不能用k>number[mouth]来判断与下一月份的天数大小,因为当mouth为12时,数组会超出下标
            mouth++;
            k = k - number[mouth - 1]; //继续减掉当前月份的天数
        }
        date = k + number[mouth -
                                1]; //循环退出,说明k<=0,此时再加上月份的天数,date就是日期
        printf("%d-%02d-%02d\n", year, mouth, date);
    }
}

????累加天数

✨练习原题

【C++】类和对象(下篇)(万字)

  • C++方法 实现一个日期类的累加,返回相加后的日期结果,那就必须对日期类的+运算符做重载
  • 然后我们对输出运算符做重载,便于输出,重载时要注意,输出运算符最好重载为友元函数,并且我们根据输出要求,不满10的月份和年份前添加0.
  • 在主函数输入时要注意是循环输入
#include<iostream>
using namespace std;
class Date
{
protected:
	friend ostream& operator<<(ostream& out, const Date& d);
public:
    Date(int year,int month,int day);
    Date operator+(int n);
    int GetMonthDay(int year, int month);
private:
    int _year;
    int _month;
    int _day;
};

Date::Date(int year,int month,int day)
{
    _day=day;
    _month=month;
    _year=year;
}
ostream& operator<<(ostream& out, const Date& d)
{
    if(d._month<10&&d._day<10)
        out << d._year << "-" <<0<< d._month << "-" <<0<< d._day;
    else if(d._month>=10&&d._day<10)
        out << d._year << "-" << d._month << "-" <<0<< d._day;
    else if(d._month<10&&d._day>=10)
        out << d._year << "-" <<0<< d._month << "-" << d._day;
    else
        out << d._year << "-" << d._month << "-" << d._day;
    return out;
}
Date Date::operator+(int n)
{
	int year = _year;
	int month = _month;
	int day = _day;
	int days = GetMonthDay(year, month);

	while (day + n > days)
	{
		month++;
		if (month > 12)
		{
			year++;
			month = 1;
		}
		n -= days;
		days = GetMonthDay(year, month);
	}
	day += n;
	return Date(year, month, day);
}

int Date::GetMonthDay(int year, int month)
{
    static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
	int day = days[month];
	if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
	{
		day += 1;
	}
	return day;
}

int main()
{
    
    int count=0;
    int year=0,month=0,day=0;
    int n=0;
     cin>>count;
    for(int cur=0;cur<count;cur++)
    {
        cin>>year>>month>>day>>n;
        Date d(year,month,day);
        Date d1=d+n;
        cout<<d1<<endl;
}

    
    return 0;
}