类和对象(下)

x33g5p2x  于2022-06-20 转载在 其他  
字(12.5k)|赞(0)|评价(0)|浏览(683)

前言

哈喽大家好,我是鹿 九 丸 \color{red}{鹿九丸}鹿九丸,今天给大家带来的是类和对象(下)。
如果大家在看我的博客的过程中或者学习的过程中以及在学习方向上有什么问题或者想跟我交流的话可以加我的企鹅号:2361038962 \color{red}{2361038962}2361038962,或者寄邮件到相应的邮箱里:2361038962 @ q q . c o m \color{red}{2361038962@qq.com}2361038962@qq.com,我会尽量帮大家进行解答!

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

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

1.2 初始化列表

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

//函数体内初始化
Date(int year = 1, int month = 1, int day = 1)
{
	_year = year;
	_month = month;
	_day = day;
}
//初始化列表
Date(int year = 1, int month = 1, int day = 1)
	:_year(year)
	, _month(month)
	, _day(day)
{
    //此时大括号中还能放东西
}
//我们甚至可以这样混着用
Date(int year = 1, int month = 1, int day = 1)
	:_year(year)
	, _month(month)
{
    _day = day;
}

结论:初始化列表可以认为就是对象成员定义的地方。

【注意】

  1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)(为什么只能初始化一次?因为那是定义的地方!
  2. 类中包含以下成员,必须放在初始化列表位置进行初始化:(除了下面三种类型,其它类型既可以在初始化列表进行初始化,也可以在函数体内初始化)
  • 引用成员变量
  • const成员变量
  • 自定义类型成员(该类没有默认构造函数)
int iref = 10;
class A
{
public:
	A(int a)
		:_a(a)
	{}
private:
	int _a;
};
class B
{
public:
	B(int a)
		:_aobj(a)
		, _ref(iref)
		, _n(10)
	{}
private:
	A _aobj; // 没有默认构造函数
	int& _ref; // 引用
	const int _n; // const 
};

问:对于没有默认构造函数的自定义类型的成员,假如不采用初始化列表的方式,我们还有什么办法来对其进行初始化?

答:

B(int a)
{
	A aa(a);//A是自定义类型的类型名
	_aa = aa;//_aa是自定义类型的成员变量名
}

注意:初始化列表中的参数可以有四种来源:

  • 形参。
A(int a)
    :_a(a)//_a是成员变量
{
    
}
  • 全局变量。
A()
    :_ref(iref)//ref是成员变量
{
        
}
  • 常量值。
A()
    :_b(10)//_b是成员变量
{
       
}
  • C99中声明时的初始化。
class A
{
public:
	A(int a)
	{

	}
private:
	int _a = 10;
};
//此时我们并未显式写出初始化列表,但上面的代码在编译器看来应该是像下面一样的
class A
{
public:
	A(int a)
        :_a(10)
	{

	}
private:
	int _a = 10;
};

注意点1:

下面这种写法是错误的:

class A
{
public:
	A(int a)
		:_a(a = 10)
	{
		cout << _a << endl;
	}
private:
	int _a;
};
int main()
{
	A a1;
	return 0;
}

这种写法编译器会报错,编译器会提示没有合适的默认构造函数可用,于是我们进行下面的修改:

class A
{
public:
	A(int a = 20)
		:_a(a = 10)
	{
        _a = a;
        cout << a << endl;
		cout << _a << endl;
	}
private:
	int _a;
};
int main()
{
	A a1;
	return 0;
}

输出结果如下:

分析:初始化列表中的赋值表达式a = 10成功执行了,所以a被赋值给了10,同时返回值a即10将_a初始化为10,然后再执行_a = a,这个语句执行了又相当于没有执行,并没有对最终结果产生影响。

再看下面的代码:

class A
{
public:
	A(int a = 20)
		:_a(a = 10)
	{
		_a = a;
		cout << a << endl;
		cout << _a << endl;
	}
private:
	int _a;
};
int main()
{
	A a1(50);
	return 0;
}

输出结果:

分析:50被赋值给形参a,所以并没有对最终的结果产生任何的影响。

此时再看下面的代码:

class A
{
public:
	A(int a = 20)
		:_a(a = 10)
	{
		_a = a;
		cout << a << endl;
		cout << _a << endl;
	}
private:
	int _a = 50;
};
int main()
{
	A a1;
	return 0;
}

输出结果:

输出结果仍然为10,此时我们可以得出一个结论:声明中初始化给的缺省值的优先级(_a = 50)比我们在初始化列表中显式给的a的优先级要低。声明中给的初始化的值是备用选择。类似于我们通常定义的函数中的缺省值一样。

结论:C++11在类的定义时对于成员变量给的缺省值是作用在初始化列表中的。

结论:初始化列表无论什么情况下都会走一遍,无论我们是否显式的给出。无论我们给的构造函数是否会形成默认构造函数,编译器都会在初始化列表中对自定义类型调用它的默认构造函数进行初始化。例如下面的代码:

class Time
{
public:
	Time()
	{
		_hour = 1;
		_minute = 1;
		cout << _hour << "-" << _minute << endl;
	}
private:
	int _hour;
	int _minute;
};
class Date
{
public:
	Date(int year,int month,int day)
	{
		_year = year;
		_month = month;
		_day = day;
		cout << _year << "-" << _month << "-" << _day;
	}
private:
	int _year;
	int _month;
	int _day;
	Time t1;
};
int main()
{
	Date d1(2022, 5, 23);
	return 0;
}

程序运行结果:

分析:我们上面的Date构造函数并不是三种默认构造函数的一种,但还是对自定义类型调用了自定义类型成员变量t1即Time类的默认构造函数。

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

注意:之前我们提到,有三种默认构造函数:全缺省构造函数、无参构造函数、我们不写时编译器默认生成的构造函数,无论是上面的哪一种,都会对自定义类型默认调用它们的构造函数,这个过程就是在初始化列表完成的,至于内置类型,因为没有值去初始化,所以呈现出毫无意义的数值。

注意下面一种情况:

class Date
{
public:
	Date(const Date& q)//拷贝构造函数
	{}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	return 0;
}

上面的代码程序会报错:没有合适的默认构造函数可用。

注意:拷贝构造函数本身也是构造函数,不过是特殊的构造函数,但是拷贝构造函数一旦出现,编译器也将不再生成默认的构造函数。上面的代码中出现了拷贝构造函数,所以默认构造函数就不再自动生成,所以编译器会报错。

  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 随机值

运行结果:

所以正确答案应该是D,为什么?因为成员变量在初始化列表中的初始化顺序只和成员变量在类中的声明的顺序有关,在上面的例子中,_a2先声明的,所以在初始化列表中先执行的是\_a2(\_a1)(此时_a1是随机值,因为并没有进行初始化),所以_a2是随机值,然后再执行的是\_a1(a),然后_a1被初始化为1,所以输出结果为1 随机值

1.3 explicit关键字

构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有类型转换的作用

class Date
{
public:
	Date(int year)
		:_year(year)
	{}
private:
	int _year;
};
void TestDate()
{
	Date d1(2022);

	// 用一个整形变量给日期类型对象赋值
	// 实际编译器背后会用2022构造一个无名对象,最后用无名对象给d2对象进行赋值
	Date d2 = 2022;
}

画图来理解:

在上面的这个过程中会调用两个函数:构造函数 + 拷贝构造(后面同类型变量的赋值是调用的是拷贝构造函数,因为是创建一个新的对象d2)

当然,在这个地方很多编译器会进行一个优化,会直接使用2022来构造一个Date类型的d2变量,省去了中间类型转换和临时变量的过程,只会调用一次构造函数。

explicit关键字的作用:用explicit修饰构造函数,将会禁止单参构造函数的隐式转换。。即无法将int类型转换成Date类型生成临时变量,继续运行上面的代码程序会报错,因为无法进行类型转换的操作。

看下面的代码:

class Date
{
public:
	Date(int year)
	{
		_year = year;
	}
private:
	int _year;
};
void TestDate()
{
	Date d1(2022);
	Date& d2 = 2022;
}
int main()
{
	TestDate();
	return 0;
}

上面的代码运行后会出现问题,为什么?因为临时变量具有常性,不能被普通的引用所引用,这属于权限的扩大,如果想要引用临时变量,只能用常引用来引用,像下面代码所演示:

class Date
{
public:
	Date(int year)
	{
		_year = year;
	}
private:
	int _year;
};
void TestDate()
{
	Date d1(2022);
	const Date& d2 = 2022;
}
int main()
{
	TestDate();
	return 0;
}

注意:这个临时变量被引用之后只有出了作用域之后才会被彻底销毁!

那么这种语法有什么用呢?看下面的代码:

void Func(const std::string& s)
{

}
int main()
{
	std::string s1 = "hello";
	Func(s1);//这种传参可以是毋庸置疑的
	Func("hello");//有了临时变量进行隐式类型转换后这种传参也变得合法,且使传参变得更加方便
    //"hello"发生了隐式类型转换后形成了一个临时变量,然后这个临时变量具有常性,类型为const string,所以必须用常引用来接收
}

2. static成员

2.1 概念

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

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

首先先看下面一个面试题,在下面的程序代码中,类型A的拷贝和构造函数被调用了多少次?

class A{};
A Func(A a)
{
	A copy(a);
	return copy;
}
int main()
{
	A a1;
	A a2 = Func(a1);
}

我们通过修改下面的代码就能够计算出调用次数:

int count = 0;
class A
{
public:
	A()
	{
		count++;
	}
	A(const A& aa)
	{
		count++;
	}
};

但是全局变量并不好,因为全局变量一般定义在.h文件里,常常会包含在其它的.cpp文件中,所以往往在项目中容易出现问题,我们推荐用静态成员变量:

class A
{
public:
    void Print()
    {
        cout << _count << endl;//类内直接访问即可
    }
	A()
	{
		count++;
	}
	A(const A& aa)
	{
		count++;
	}
private:
    static int _count;//声明
};
int A::_count = 0;//定义,静态成员变量只能在类外定义
//假如_count是public权限的,在类外有两种访问方式:
Date d1;
d1._count;//通过具体某个类的实例化对象来进行访问
A::_count;//通过类来进行访问

问:静态成员变量和普通成员变量有什么区别?

答:静态成员变量不占用栈区上的空间,存在于静态区,属于整个类,属于类的所有对象,而不属于某个对象,当我们用sizeof()求某个类的成员或者类类型的大小的时候,不会包括静态成员变量。例如:

class A
{
public:
	A()
	{
		count++;
	}
	A(const A& aa)
	{
		count++;
	}

private:
	static int count;
};

int main()
{
	cout << sizeof(A) << endl;
}

输出结果为1。

问:那么静态成员变量是否在初始化列表中进行初始化的呢?

答:不是,因为只有具体的对象在初始化的时候才会在初始化列表进行初始化。

除了成员变量可以是静态的,成员函数也可以是静态的,成员函数最为重要的特征是:没有this指针。

我们在类外访问静态成员函数有两种方式:

class A
{
public:
	static void Func()
	{

	}
};
int main()
{
	A a1;
	a1.Func();//使用类的实例化成员来进行调用静态成员函数
	A::Func();//使用类的类名来调用静态成员函数
	return 0;
}

上面两种调用方式无论是哪一种,都只是为了突破类域。

2.2 特性

  1. 静态成员为所有类对象所共享,不属于某个具体的实例
  2. 静态成员变量必须在类外定义,定义时不添加static关键字
  3. 类静态成员即可用类名::静态成员或者对象.静态成员来访问
  4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
  5. 静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以具有返回值
  6. 未初始化的静态成员变量会默认为0,但是一定要显式定义,可以不初始化。

【问题】

  1. 静态成员函数可以调用非静态成员函数吗?
  2. 非静态成员函数可以调用类的静态成员函数吗?

3.C++11 的成员初始化新玩法。

C++11支持非静态成员变量在声明时进行初始化赋值,但是要注意这里不是初始化,这里是给声明的成员变量缺省值。

注意:静态成员变量无法赋缺省值,只能在类外进行定义并初始化,为什么?因为此处只是声明,并不是真正的定义,初始化列表只有在显式创建对象的时候才会用到。

下面的代码就是错误的:

class A
{
private:
    int _a = 1;
    static int _ref = 10;//此处就是错误的,因为静态成员变量只能在类外进行定义并初始化
};
//下面才是正确的
class A
{
private:
    int _a = 1;
    static int _ref = 1;//此处就是错误的,因为静态成员变量只能在类外进行定义并初始化
};
int A::_ref = 10;

4. 友元

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

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

4.1 友元函数

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

友元函数的经典使用:

class Date
{
	friend ostream& operator<<(ostream& out, const Date& d);//在operator<<函数内可以在类外访问Date类的成员变量
public:
	Date(int year,int month,int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
ostream& operator<<(ostream& out,const Date& d)
{
	out << d._year << "-" << d._month << "-" << d._day << endl;
	return out;
}

int main()
{
	Date d1(2022, 5, 20);
	cout << d1;
	return 0;
}

说明:

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

下面是一个例子:

class Date;
//问:此处为什么要加一个前置声明?
//答:因为在Time里面对Print友元函数的声明中,向前找是找不到Date类的声明或者定义的,所以必须在此处声明Date是一个类的名字,关于具体的Date类的定义可以去后面找
class Time
{
	friend void Print(const Date& d, const Time& t);//Print是Time类的友元函数,在Print函数内可以访问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
{
	friend void Print(const Date& d, const Time& t);//Print是Date类的友元函数,在Print函数内可以访问Date类的成员变量
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};
void Print(const Date& d,const Time& t)
{
	cout << d._year << "-" << d._month << "-" << d._day << "-";
	cout << t._hour << "-" << t._minute << "-" << t._second << endl;
}
int main()
{
	Date d1(2022, 5, 20);
	Time t1;
	Print(d1, t1);
	return 0;
}
  • 友元函数的调用与普通函数的调用和原理相同

注意:下面的这种友元函数的使用方法是错误的,且目前C++没有这种语法来实现:

class Time
{
	friend void Date::Print();
public:
	Time(int hour = 0, int minute = 0, int second = 0)
	{
		_hour = hour;
		_minute = minute;
		_second = second;
	}
	int _hour;
	int _minute;
	int _second;
};
class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << "-";
		cout << _t._hour << "-" << _t._minute << "-" << _t._second;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};
int main()
{
	Date d1(2022, 5, 20);
	d1.Print();
	return 0;
}

如果我们想在Date类中访问Time类中的成员变量,只有一种方法,就是将Date声明为Time的友元类,出吃之外没有其它的方法,即C++不支持将某个类中的成员函数声明为某个类的友元函数。

4.2 友元类

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

友元类的经典使用:

class Time
{
	friend class Date;//Date是Time的友元类,可以在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, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << "-";
		cout << _t._hour << "-" << _t._minute << "-" << _t._second << endl;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};
int main()
{
	Date d1(2022, 5, 20);
	d1.Print();
	return 0;
}
  • 友元关系是单向的,不具有交换性。

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

  • 友元关系不能传递

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

class A
{
	friend class B;
};
class B
{
	friend class C;
};
class C
{

};

此时在C中可以访问到B的成员变量和成员函数,但是不能在C中访问到A的成员变量和成员函数。

5. 内部类

5.1概念及特性

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

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

特性:

  1. 内部类可以定义在外部类的public、protected、private都是可以的。
  2. 注意内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
  3. sizeof(外部类)=外部类,和内部类没有任何关系。
  4. 通过内部类进行定义变量的时候要注意加上外部类限定符。
class A
{
public:
    class B
    {
        
    }
}
int main()
{
    A::B b1;//B是A的内部类,定义B类型的变量的时候要在B的前面加上外部类限定符即A
    return 0;
}

6. 练习题

  1. 求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句

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

使用内部类:

class Solution {
public:
    int Sum_Solution(int n) {
        Sum a[n];
        return _ret;
    }
    class Sum
    {
    public:
        Sum()
        {
            _ret += _i;
            _i++;
        }
    };
private:
    static int _ret;
    static int _i;
};
int Solution::_i = 1;
int Solution::_ret = 0;
  1. 计算日期到天数的转换

代码:

#include<iostream>
using namespace std;
bool IsLeapYear(int year)
{
    if((year % 4 == 0 && year % 100 != 0)|| year % 400 == 0)
    {
        return true;
    }
    else
    {
        return false;
    }
}
int main()
{
    int SumMonthDay[13] = {0,31,59,90,120,151,181,212,243,273,304,334,365};//存储前n-1个月的天数和
    int year = 0;
    int month = 0;
    int day = 0;
    int ret = 0;
    cin >> year >> month >> day;
    ret = SumMonthDay[month -  1] + day;//前n - 1个月的和加上当前月的天数
    if(month > 2 && IsLeapYear(year))//判断是否是闰年,如果是闰年并且前面跨过了2月就加一天
    {
        ret += 1;
    }
    cout << ret;
    return 0;
}
  1. 打印日期

代码:

#include<iostream>
using namespace std;
bool IsLeapYear(int year)
{
    if((year % 4 == 0 && year % 100 != 0)|| year % 400 == 0)
    {
        return true;
    }
    else
    {
        return false;
    }
}
int main()
{
    //都按照平年来计算
    int SumMonthDay[13] = {0,31,59,90,120,151,181,212,243,273,304,334,365};//存储前n-1个月的天数和
    int year = 0;
    int day = 0;
    cin >> year >> day;
    for(int i = 0;i < 13; i++)
    {
        
        if(day <= SumMonthDay[i])
        {
            if(i > 2 && IsLeapYear(year))//判断是否超过2月并判断是否是闰年
            {
                day -= 1;  //如果是闰年就减一天变成平年和一样的算法
            }
            day -= SumMonthDay[i - 1];//减去跨过的天数就是剩余当前月的天数
            
            printf("%d-%02d-%02d",year, i, day);
            break;
        }
    }
    
    return 0;
}
  1. 累加天数

代码:

#include<iostream>
using namespace std;
int GetMonthDay(int year,int month)
{
    int MonthDay[13] = {0,31,28,31,30,31,30,31,31,30,31,30,31};//存储每个月对应的天数
    if(month == 2 && (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))//判断是否是2月和是闰年
        return 29;
    return MonthDay[month];
}
int main()
{
    int num = 0;
    cin >> num;
    int year = 0;
    int month = 0;
    int day = 0;
    
    int x = 0;//存储要加的天数
    for(int i = 0; i < num; i++)
    {
        cin >> year >> month >> day >> x;
        day += x;
        while(day > GetMonthDay(year, month))
        { 
            day -= GetMonthDay(year, month);
            month++;
            if(month == 13)
            {
                month = 1;
                year++;
            }
        }
        printf("%4d-%02d-%02d\n",year, month, day);
    }
    return 0;
}

7. 再次理解封装

C++是基于面向对象的程序,面向对象有三大特性即:封装、继承、多态。

C++通过类,将一个对象的属性与行为结合在一起,使其更符合人们对于一件事物的认知,将属于该对象的 所有东西打包在一起;通过访问限定符选择性的将其部分功能开放出来与其他对象进行交互,而对于对象内部的一些实现细节,外部用户不需要知道,知道了有些情况下也没用,反而增加了使用或者维护的难度,让整个事情复杂化。

8. 再次理解面向对象

可以看出面向对象其实是在模拟抽象映射现实世界。

相关文章