【C++学习笔记】类和对象下篇

🙊 初始化列表🙊

💖 构造函数体赋值

在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。

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

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

💖 初始化列表

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


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

为什么会有初始化列表呢?

看下面一段代码,aa 对象整体的定义在 main 函数中,那么既然对象整体定义了,每个成员什么时候定义呢?

class A
{
public:
		//声明
		int _a1;
		int _a2;
};
int main()
{
	A aa; //对象整体的定义
	return 0;
}

如果将 A 类里面成员变量加一个 const int x,那么程序就不会运行

class A
{
public:

private:
		//声明
		int _a1;
		int _a2;
		const int x;
};
int main()
{
	A aa; //对象整体的定义
	return 0;
}

因为 const 变量必须在定义的地方初始化,默认生成的构造函数对内置类型不做处理,而 const int x 是内置类型,所以编译器没有进行处理,而我们知道 const 必须进行初始化。所以必须给每个成员找一个定义的位置,不然像 const 成员不好处理,就提出了初始化列表。

当然也可以选择采用缺省值的方式进行初始化,但是缺省值只在 C++11 之后才支持,不管给不给缺省值,都会去执行初始化列表。

class A
{
public:
	A()
		:x(1)
		{
			_a1++;
		}
private:
		//声明
		int _a1 = 1;
		int _a2 = 2;
		const int x;
};
int main()
{
	A aa; //对象整体的定义
	return 0;
}

通过上述例子我们知道:

1、初始化列表是所有成员变量定义的位置,哪个对象定义调用构造函数,初始化列表就是那个对象所有成员变量定义的位置

2、不管以显示的方式在初始化列表写,每个变量都会在初始化列表定义初始化

再来看下面一段代码

class B
{
public:
	B(int b)
		:_b(0)
	{
		cout << "B()" << endl;
	}
private:
	int _b;
};

class A
{
public:
	A()
		:_x(1)
		, _a2(1)
		, _ref(_a1)
	{
		_a1++;
		_a2--;
	}

private:
	int _a1 = 1; // 声明
	int _a2 = 2;
	const int _x;
	int& _ref;
	B _bb;
};
int main()
{
	A aa;
	return 0;
}

问题:

1、_bb 会不会被初始化?

_bb 会被初始化,因为编译器对自定义类型会调用其默认构造,在 a 的初始化列表调用默认构造函数,注意默认构造是不传参数就能调用的构造函数。

【注意】:

  1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
  2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
    (1)  引用成员变量,因为引用必须在定义的地方进行初始化
    (2)  const 成员变量
    (3)  自定义类型成员(且该类没有默认构造函数时)
  3. 所有成员能在初始化列表初始化就在初始化列表初始化
  4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其初始化列表中的先后次序无关

🙊类型转换🙊

💖 单参数类型的隐式转换

看下面一段代码,可以通过下面两种方式进行初始化,第一种方式是直接调用构造函数,而第二种方式是做了一个类型转换,自定义类型有了单参数的构造函数后就可以以类型转换的方式进行初始化。

class A
{
public:
	A(int a)
		:_a1(a)
		{}
private:
	int _a2;
	int _a1;
};
int main()
{
	A aa1(1); //构造函数
	A aa2 = 1; //类型转换
	return 0;
}

如果不理解,先看以下这个例子

int main()
{
	int i = 1;
	double d = i;
	return 0;
}

以上内容发生了隐式类型转换,i 将其赋值给一个具有常性 double 类型的临时变量,临时变量再赋值给 d。再回过来看第一段代码,我们知道先调用构造函数创建一个 A 类型的临时对象,再用拷贝构造赋值给 aa2

在这里插入图片描述

为了验证,我们写出如下代码:

class A
{
public:
	A(int a)
		:_a1(a)
		{
		cout << "A(int a)" << endl;
		}
	A(const A& aa)
	:_a1(aa._a1);
	{
	cout << "A(const A& aa)" << endl;
	}
private:
	int _a1;
};
int main()
{
	A aa1(1); //构造函数
	A aa2 = 1; //类型转换
	return 0;
}

注意上面代码中拷贝构造也有初始化列表,因为拷贝构造也是一个构造,构造都可以用初始化列表。按照我们的理解,上述代码运行应该是两个构造,一个拷贝构造,运行结果如下:
在这里插入图片描述
为什么这里只有两个构造函数呢?

因为 c++ 针对自定义产生临时对象的情况会进行优化,情形一的情况:用 1 构造一个临时对象,再去进行拷贝构造,被优化成了直接用 1 构造 aa1

再看以下情形:

class A
{
public:
	A(int a)
		:_a1(a)
		{
		cout << "A(int a)" << endl;
		}
	A(const A& aa)
	{
		:_a1(aa._a1);
	cout << "A(const A& aa)" << endl;
	}
private:
	int _a1;
};
int main()
{
	A& ref = 10;
	return 0;
}

这里我们知道,A 类型不能引用整型 10,因为 10 是左值,所以这里报错了。
在这里插入图片描述

但是我们加了 const 修饰,看能否编译通过

int main()
{
	const A& ref = 10;
	return 0;
}

结果如下:

在这里插入图片描述
因为这里是引用,所以编译器不进行优化,产生了临时对象,即先用构造函数创建一个临时对象,临时对象具有常性(参考链接)而修改一个临时对象毫无意义,所以C++编译器加入了临时变量只能作为 const 引用类型对象的实参这个语义限制,让这个参数在函数中不能被修改,意在限制这个非常规用法的潜在错误。
所以如果有单参数的构造函数,就支持隐式类型的转换。

💖 多参数类型的隐式转换

c++ 98 支持单参数的构造函数的类型转换,而 c++ 11 支持多参数构造函数的隐式类型转换如果是多参数构造函数的隐式转换呢,可以写成以下形式

class A
{
public:
	A(int a)
		:_a1(a)
		{
		cout << "A(int a)" << endl;
		}
	A(const A& aa)
	{
		:_a1(aa._a1);
	cout << "A(const A& aa)" << endl;
	}
	A(int a1, int a2)
		:_a1(a1)
		,_a2(a2)
	{}
private:
	int _a1;
	int _a2;
};
int main()
{
	//单参数的构造函数
	A aa1(1);
	//多参数的构造函数
	A aa2 = {1,1};
	return 0;
}

💖 explicit 关键字

如果构造函数不想使用这种隐式转换,可以添加关键字 explicit 禁用隐式转换。

class A
{
public:
	explicit A(int a)
		:_a1(a)
		{
		cout << "A(int a)" << endl;
		}
	A(const A& aa)
	{
		:_a1(aa._a1);
	cout << "A(const A& aa)" << endl;
	}
	explicit A(int a1, int a2)
		:_a1(a1)
		,_a2(a2)
	{}
private:
	int _a1;
	int _a2;
};
int main()
{
	//单参数的构造函数
	A aa1(1);
	//多参数的构造函数
	A aa2 = {1,1};
	return 0;
}

🙊static 静态成员🙊

💖 static 静态成员概念

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

💖 特性

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

💖 举例说明

实现一个类,计算程序中创建出了多少个类对象。这里我们可以直接考虑用构造函数,因为 A 类型的对象通过调用构造函数或者拷贝构造函数被创建,我们需要定义一个全局变量 count,然后在构造函数里面 ++count,最后就知道调用了几次构造函数创建对象了。

class A
{
public:
		A(int a = 0)
		{
			++count;
		}
		A(const A&aa)
		{
			++count;
		}
};
void func(A a)
{}
int main()
{
	A aa1;
	A aa2(aa1);
	func(aa1);
	A aa3 = 1;
	cout << count << endl;
	return 0;
}

但是编译运行代码就报错了,为什么呢?

在这里插入图片描述
出现了命名冲突的问题,因为 c++ 的某个文件里面有一个函数名叫 count,现在我们又定义了一个count 就造成了冲突,解决办法之一就是不去展开命名空间,这样就不会和库里面的函数有冲突,但是这样做又会出现一个问题,就是如果我们又写了一个语句对 count 进行操作,那么最后 count 的值就不准了。
所以在这了 c++ 为了解决这一系列问题,可以将 count 变量放到类里面,而放到类里面就变成了类的一个成员,就属于某个对象而不是整个类,为了让其属于整个类,我们增加了一个静态成员的概念

class A
{
public:
		A(int a = 0)
		{
			++count;
		}
		A(const A&aa)
		{
			++count;
		}
};
private:
	//不属于某个对象,是属于所有对象属于整个类
	static int count;//声明
	//不能使用缺省值的方式初始化
	//static int count = 0;
}
int A::count = 0;//类域定义初始化
int main()
{
	A aa1;
	A aa2(aa1);
	func(aa1);
	A aa3 = 1;
	cout << A::count << endl;
	cout << aa1.count << endl;
	return 0;
}

注意静态成员变量不能使用缺省值初始化,因为缺省值是给构造函数初始化列表使用的,而初始化列表是属于对象的每个成员定义的地方,而这里的 count 是属于共有的,所以要使用类外初始化的方式。而注意输出打印 count 的时候,如果是公有成员,可以以下面的方式访问。

int main()
{
	A aa1;
	A aa2(aa1);
	func(aa1);
	A aa3 = 1;
	cout << A::count << endl;
	cout << aa1.count << endl;
	return 0;
}

因为 count 既属于一个类,也属于某个对象,两种方式都能帮助 count 突破类域,告诉编译器去哪里找 count。但是如果是私有成员就只能写一个函数接口去间接访问 count

class A
{
public:
		A(int a = 0)
		{
			++count;
		}
		A(const A&aa)
		{
			++count;
		}
};
	int GetCount()
	{
		return count;
	}
private:
	//不属于某个对象,是属于所有对象属于整个类
	static int count;//声明
	//不能使用缺省值的方式初始化
	//static int count = 0;
}
int A::count = 0;//类域定义初始化
int main()
{
	A aa1;
	A aa2(aa1);
	func(aa1);
	A aa3 = 1;
	cout << aa3.GerCount() << endl;
	return 0;
}

💖 static 静态成员函数

如果想要访问静态成员变量就需要静态成员函数,因为静态成员函数没有 this 指针所以不能访问非静态成员。

class A
{
public:
		A(int a = 0)
		{
			++count;
		}
		A(const A&aa)
		{
			++count;
		}
};
	static int GetCount()
	{
		//_a++;  //不能直接访问非静态成员
		return count;
	}
private:
	//不属于某个对象,是属于所有对象属于整个类
	static int count;//声明
	//不能使用缺省值的方式初始化
	//static int count = 0;
}
int A::count = 0;//类域定义初始化
void func()
{
	A aa1;
	A aa2(aa1);
	A aa3 = 1;
}
int main()
{
	func();
	cout << A::GerCount() << endl;
	return 0;
}

💖 例题

求 1+2+3+ … + n,要求不能使用乘除法、for、while、if、else、switch、case 等关键字及条件判断语句。
定义一个 Sum 类,里面有静态成员变量 _i 和 _sum,利用静态成员变量与静态成员函数的概念来解决这个问题。当然可以用变长数组的方式来写,也可以不用变长数组的写法,代码如下:

class Sum
{
public:
	Sum()
	{
		_sum += _i;
		++_i;
	}

	static int GetSum()
	{
		return _sum;
	}
private:
	static int _i;
	static int _sum;

};

int Sum::_i = 1;
int Sum::_sum = 0;

class Solution
{
public:
	int Sum_Solution(int n)
	{
		//Sum a[n]; //变长数组写法
		Sum *ptr = new Sum[10];
		return Sum::GetSum();
	}
};
int main()
{
	Solution s;
	cout << s.Sum_Solution(10) << endl;
	return 0;
}

🙊匿名对象🙊

匿名对象的特点就是没有名字,代码如下

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

匿名对象只在当前行有用,执行完当前行就被销毁,如果对象有名字,就可以在当前域内使用,如果想临时使用一次,就用匿名对象。

🙊友元🙊

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

💖 友元函数

问题:现在尝试去重载 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;
}

说明:

  1. 友元函数可访问类的私有和保护成员,但不是类的成员函数
  2. 友元函数不能用 const 修饰,因为 const 修饰的是 *this,只有非静态成员函数可以用 const 修饰
  3. 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  4. 一个函数可以是多个类的友元函数
  5. 友元函数的调用与普通函数的调用原理相同

💖 友元类

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

  1. 友元关系是单向的,不具有交换性。
    比如上述 Time 类和 Date 类,在 Time 类中声明 Date 类为其友元类,那么可以在 Date 类中直接访问 Time 类的私有成员变量,但想在 Time 类中访问 Date 类中私有的成员变量则不行。
  2. 友元关系不能传递
    如果 CB 的友元,BA 的友元,则不能说明 CA 的友元。
  3. 友元关系不能继承,在继承位置再给大家详细介绍。

看下面一段代码,一个日期类中有一个 Time 类的私有成员,在进行 Time 类初始化的时候,由于 Time 权限私有,可以加一个友元声明,让 Date 可以访问 Time

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、如果一个类定义在另一个类的内部,这个内部类就叫做内部类。
2、内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。
3、外部类对内部类没有任何优越的访问权限。

注意:

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

特性:

  1. 内部类可以定义在外部类的 publicprotectedprivate 都是可以的。
  2. 注意内部类可以直接访问外部类中的 static 成员,不需要外部类的对象 / 类名。
  3. sizeof ( 外部类 ) = 外部类,和内部类没有任何关系。

注意看以下代码的运行结果是多少:

class A
{
private:
		int h;
public:
		//内部类
		class B
		{
		private:
				int b;
		};
};
int main()
{
	A aa;
	A :: B bb;
	cout << sizeof(aa) << endl;
}

可以看到此段代码的结果为 4,即 aa 的大小为 4,说明 aa 里面只有自己的成员而没有 B,它只受 A 类域的限制,同时也受到访问限定符的限制。如果我们要定义一个 B 对象,因为编译器不会去类和命名空间中搜索,这里想要创建一个 B 对象需要指定去 A 域里面搜索。

可以根据自己的需求使其设置为共有或者私有。

在这里插入图片描述

🙊编译器在有些场景的优化🙊

在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还是非常有用的。

class A
{
public:
 A(int a = 0)
 :_a(a)
 {
 cout << "A(int a)" << endl;
 }
 A(const A& aa)
 :_a(aa._a)
 {
 cout << "A(const A& aa)" << endl;
 }
 A& operator=(const A& aa)
 {
 cout << "A& operator=(const A& aa)" << 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;
}

void func2(const A& aa)
{
}
int main()
{
 // 传值传参
 A aa1;
 f1(aa1);
 cout << endl;
 // 传值返回
 f2();
 cout << endl;
 // 隐式类型,连续构造+拷贝构造->优化为直接构造
 f1(1);
 // 一个表达式中,连续构造+拷贝构造->优化为一个构造
 f1(A(2));
 cout << endl;
 // 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
 A aa2 = f2();
 cout << endl;
 // 一个表达式中,连续拷贝构造+赋值重载->无法优化
 aa1 = f2();
 func2(aa1); //引用传参无优化
 func2(2);//构造一个临时匿名对象,不优化
 func2(A(3));//不优化
 cout << endl;
 return 0;
}

再看下面这个场景

A fun()
{
	A aa;
	return aa;
}
int main()
{
	A aa1 = fun();
	return 0;
}

根据之前的经验,这里是一个构造和两个拷贝构造,但是由于这是一个连续的步骤,编译器优化为直接进行一个构造和一个拷贝构造。那么直接返回一个匿名对象这种情况呢?

A fun()
{
	return A();
}
int main()
{
	fun();
	return 0;
}

用构造函数创建一个临时对象,然后再拷贝构造返回,被优化成了直接进行构造。

总结:
1、接收返回值对象,尽量拷贝构造方式接收,不要赋值接收
2、函数中返回对象时,尽量返回匿名对象
3、函数传参尽量使用 const & 传参

🙊 再次理解类和对象 🙊

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

  1. 用户先要对现实中洗衣机实体进行抽象—即在人为思想层面对洗衣机进行认识,洗衣机有什么属性,有那些功能,即对洗衣机进行抽象认知的一个过程
  2. 经过 1 之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言(比如:C++、Java、Python 等) 将洗衣机用类来进行描述,并输入到计算机中
  3. 经过 2 之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才能洗衣机是什么东西。
  4. 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。

🙊 计算日期到天数的转换 🙊

计算日期到天数的转换,输入年、月、日,输出这是一年中的第几天,可以用面向过程的思想编程,代码如下:

int main()
{
	int year,month,day;
	
	cin >> year >> month >> day ;
	
	int monthDay1_N[13] = {0,31,59,90,120,151,181,212,243,
	273,304,334,365};
	
	int nday = monthDay1_N[ month - 1];
	
	nday += day;
	
	if(month > 2 && (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
	{
		nday += 1;
	}
}
	cout << nday << endl;

🙊 总结 🙊

总结:

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值