C++初阶:类和对象(下)

✨✨小新课堂开课了,欢迎欢迎~✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

所属专栏:C++:由浅入深篇

小新的主页:编程版小新-CSDN博客

1.再探构造函数

1.1构造函数体内赋值

之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值。

cpp 复制代码
class Date
{
public:
	//构造函数
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

需要注意的是,上面构造函数的实现是使每个成员变量都有了一个初始值,但是构造函数体的语句其实只能被认为是赋值,而不是初始化,因为初始化只能初始化一次,而赋值可以有多次。

cpp 复制代码
class Date
{
public:
	//构造函数
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;//第一次
		_year = 2024;//第二次
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

1.2初始化列表

构造函数初始化还有一种方式,就是初始化列表。

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

cpp 复制代码
class Date
{
public:
	
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};

1.3小细节

一:每个成员变量在初始化列表中只能出现一次
语法理解上初始化列表可以认为是每个成员变量定义初始化的地方,初始化只能初始一次。
二:下面几个成员变量,必须在初始化列表初始化
1.const成员变量
const变量必须在定义的时候初始化,并且只有这一次初始化机会。而初始化列表就是成员变量定义初始化的地方,所以const成员变量必须在初始化列表初始化。

初始化:

cpp 复制代码
class Date
{
public:
	
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
		, _a(1)
	{}
private:
	int _year;
	int _month;
	int _day;
	const int _a;
};

2.引用成员变量

引用必须初始化,在定义时就要给一个初始值,而初始化列表是每个成员变量定义初始化的地方,所以引用成员变量必须在初始化列表初始化。

初始化:

cpp 复制代码
class Date
{
public:
	
	Date(int& xx,int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
		, _a(1)
		, _ra(xx)
	{}
private:
	int _year;
	int _month;
	int _day;
	const int _a;
	int& _ra;
};

3.自定义类型成员

若一个类没有默认构造函数,那我们在实例化类类型对象时就要传参对其初始化,所以没有默认构造的类类型变量,必须放在初始化列表位置就要初始化,否则会编译报错。
初始化:

cpp 复制代码
 class Time
{
public:
	Time(int hour)
		:_hour(hour)
	{
		cout << "Time()" << endl;
	}
private:
	int _hour;
};

class Date
{
public:
	Date(int& xx,int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
		, _a(1)
		, _ra(xx)
		,_t(1)
	{}
private:
	int _year;
	int _month;
	int _day;
	const int _a;
	int& _ra;
	Time _t;

};

总结:在定义时就必须要初始化的变量类型,必须在初始化列表初始化。
三:C++11支持持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的成员使用的。
如果这个成员在声明位置给了缺省值,初始化列表会用这个缺省值初始化。如果你没有给缺省值:
内置类型:否初始化取决于编译器,C++并没有规定。
自定义类型:调用这个成员类型的默认构造函数,如果没有默认构造会编译错误。
下面是给了缺省值的情况:

cpp 复制代码
#include<iostream>
using namespace std;

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;
	int _month = 1;
	int _day;
	Time _t = 1;
	const int _n = 1;
	int* _ptr = (int*)malloc(12);
};

四:尽量使用初始化列表初始化
因为初始化列表实际上就是当你实例化一个对象时,该对象的成员变量定义初始化的地方,所以 无论你是否使用初始化列表对成员变量初始化,都会走初始化列表
对于内置类型,使用初始化列表或者构造函数体内赋值进行初始化时没有什么差别的。
对于自定义类型可以提高代码效率。

cpp 复制代码
class Time
{
public:
	Time(int hour = 0)
	{
		_hour = hour;
	}
private:
	int _hour;
};

class Date
{
public:
	// 使用初始化列表
	Date(int hour)
		:_t(12)// 调用一次Time类的构造函数
	{}
private:
	Time _t;
};

对于以上代码,当我们要实例化一个Date类的对象时,由于使用初始化列表进行初始化,在实例化过程中只调用了一次Time类的构造函数。

如果我们不使用初始化列表,使用构造函数体内赋值的话:

cpp 复制代码
class Time
{
public:
	Time(int hour = 0)
	{
		_hour = hour;
	}
private:
	int _hour;
};
class Date
{
public:
	// 构造函数体内赋值
	Date(int hour)
	{ 
		Time t(hour);// 调用一次Time类的构造函数
		_t = t;// 调用一次Time类的赋值运算符重载函数
              //在此之前其实还会调用一次构造函数,因为_t是一个新的Time对象,它需要进行初始化
	}
private:
	Time _t;
};

这时,我们要实例化一个Datet类的对象时,在实例化Date对象时调用一次Time类的构造函数,然后还需要调用了一次Time类的赋值运算符重载函数,这样效率就降下来了。
五.成员变量在类中的声明顺序就是初始化列表初始化的顺序
初始化顺序跟成员在初始化列表出现的的先后顺序无关,建议声明顺序和初始化列表顺序保持⼀致。
下面我们看一道练习:
A. 输出 1 1
B. 输出 2 2
C. 编译报错
D. 输出 1 随机值
E. 输出 1 2
F. 输出 2 1

cpp 复制代码
//下面程序的运行结果是什么
#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();
}

已知成员变量在类中的声明顺序就是初始化列表初始化的顺序。先声明的_a2,所以先初始化_a2,由于_a2是用_a1进行初始化的,而_a1还未被初始化,是个随机值,所以_a2最后是个随机值。后声明的_a1,然后在初始化_a1是1。这是虽然_a1是1了,但是_a2不会变成1,因为只能初始化一次,所以答案故选D。

2.隐式类型转换

我们在学习C语言时就知道,如果赋值两边的类型不同时就可能发生隐式类型转换。

隐式类型转换转换是由编译器自动进行的,无需程序员明确指定。

显示类型转换则是由程序员通过特定的语法明确指示编译器进行类型转换。

2.1内置类型

在发生隐式类型转换时,如果两边都是内置类型会生成一个临时变量(类型转换会生成临时变量)。将右操作数强制类型转换为左操作数的类型,最后用这个临时变量给左操作数赋值。临时变量具有常性,不能更改

cpp 复制代码
int main()
{
	double j = 1.1;
	int i = j;//隐式类型转换
	int& a = j;//error  权限放大
	const int& b = j;//正确 权限平移
	return 0;
}

通过引用来测试,我们得知a,b,j指向同一块空间。a,b都是j的别名,然而只用常引用是可行的,这也验证了发生类型转换时产生的临时变量具有常性。

2.2自定义类型

在发生隐士类型转换时,如果将一个内置赋值给自定义类型,编译器产生一个自定义类型的临时变量,然后会调用这个自定义类型的构造函数初始化这个临时变量,最后用这个临时变量对左操作数进行拷贝构造。临时变量也具有常性,不可修改。

cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1 = 2024;//发生隐式类型转换
	Date d2 = { 2024,8 };

	d1.Print();
	d2.Print();
	return 0;
}

2.3explicit关键字

C++支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数、 但当构造函数前面加explicit就不再支持隐式类型转换。

cpp 复制代码
#include<iostream>
using namespace std;

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;
	}
private:
	int _a1 = 1;
	int _a2 = 2;
};
int main()
{
	// 1构造一个A的临时对象,再用这个临时对象拷⻉构造aa1
	// 编译器遇到连续构造+拷⻉构造->优化为直接构造
	A aa1 = 1;
	aa1.Print();
	const A& aa2 = 1;
	// C++11之后才支持多参数转化
	A aa3 = { 2,9 };
	aa3.Print();
	return 0;
}

3.static成员

3.1定义

用static修饰的成员变量,称之为静态成员变量 ,用static修饰的成员函数,称之为静态成员函数

cpp 复制代码
class A
{
public:
    static int Print()//静态成员函数
	{
		cout << "Print()" << endl;
	}
private:
	static int _a;//静态成员变量
};

3.2小细节

1.静态成员变量一定要在类外进行定义初始化,定义时不添加static关键字。

cpp 复制代码
class A
{
public:
	static int Print()//静态成员函数
	{
		cout << "Print()" << endl;
	}
private:
	static int _a;//静态成员变量
};

int A::_a = 1;

这里静态成员变量_a虽然是私有,但是我们在类外突破类域直接对其进行了访问。这是一个特例,不受访问限定符的限制,否则就没办法对静态成员变量进行定义和初始化了。
2.静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。

cpp 复制代码
class A
{
public:
	static int Print()//静态成员函数
	{
		cout << "Print()" << endl;
	}
private:
	static int _a;//静态成员变量
};

int A::_a = 1;

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

最终计算的结果是1,因为静态成员_a存在静态区,属于整个类,也属于类的所有对象。所以在计算类的大小或则类对象的大小时,静态成员的大小并不算在其中。
3.静态成员函数没有this指针,可以访问其他的静态成员,但是不能访问非静态的。

cpp 复制代码
class Test
{
public:
	static void Fun()
	{
		cout << _a << endl; //error不能访问非静态成员
		cout << _n << endl; //正确
	}
private:
	int _a; //非静态成员
	static int _n; //静态成员
};


注意:非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
4.静态成员也是类的成员,受public、protected、private 访问限定符的限制。
所以当静态成员变量设置为private时,尽管我们突破了类域,也不能对其进行直接访问。
5.突破类域就可以访问静态成员,可以通过类名::静态成员 或者 对象.静态成员 来访问静态成员变量和静态成员函数。
1.当静态成员成员变量设置为公有时:

cpp 复制代码
class Test
{
public:
	static int _n; 
};

int Test::_n = 0;

int main()
{
	Test test;
	cout << test._n << endl; //1.通过类对象突破类域进行访问
	cout << Test::_n << endl; //2.通过类名突破类域进行访问
	return 0;
}

2.当静态成员变量被设置为私有时:

cpp 复制代码
class Test
{
public:
	static int GetN()
	{
		return _n;
	}
private:
	static int _n;
};

int Test::_n = 0;

int main()
{
	Test test;
	cout << test.GetN() << endl; //1.通过对象调用成员函数进行访问
	cout << Test::GetN() << endl; //2.通过类名调用静态成员函数进行访问
	return 0;
}

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

4.友元

友元提供了一种突破类访问限定符封装的方式,友元分为:友元函数和友元类。

在函数声明或者类声明的前面加friend,并且把友元声明放到一个类的里面。

4.1友元函数

我们有时想在类外面访问类的私有或保护成员,但是碍于访问限定符的限制,我们不能直接访问。如果一定要访问的话,我们就可以借助友元函数。

它的用法:friend+函数的声明

cpp 复制代码
class Date
{
public:
	friend void Print(const Date& d);
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	
private:
	int _year;
	int _month;
	int _day;
};
void Print(const Date& d)
{
	cout <<d._year<< "-" << d._month << "-" <<d. _day << endl;
}

注意:
友元函数仅仅是一种声明,他不是类的成员函数。
友元函数可以在类定义的任何地声方明,不受类访问限定符限制。
一个函数可以是多个类的友元函数。
友元函数不能被const修饰。
友元函数的调用和普通函数的调用原理相同。

4.2友元类

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

友元类的用法:friend+class+类名

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
	// 友元声明
	friend class B;//B类是A类的友元
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;
};
int main()
{
	A aa;
	B bb;
	bb.func1(aa);
	bb.func2(aa);
	return 0;
}

注意:
1.友元类的关系是单向的,不具有交换性。
比如上面的代码B类是A类的友元,B类可以访问A类中的成员变量和成员函数,但是A类不是B类的友元,A就不能随意访问B类。
2.友元类关系不能传递
如果A类是B类的友元, B类是C类的友元,但是A类不是C类的友元。
3.有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。

5.内部类

5.1定义

如果一个类定义在另一个类的内部,这个内部类就叫做内部类。
内部类是一个独立的类,不属于内部类,跟定义在全局相比,他只是受外部类类域限制和访问限定符限制,所以不能用外部类的对象去访问内部类的成员,外部类定义的对象中不包含内部类,并且 内部类默认是外部类的友元类。

cpp 复制代码
class A
{
public:
	class B//B类是A类的友元
	{
	private:
		int _j;
	};
private:
	int _a;
	int _b;
};

5.2小细节

1.内部类定义在外部类的public、protected、private都是可以的。
当A类跟B类紧密关联,如果放到private/protected位置,那么A类就是B类的专属内部类,其
他地方都用不了。
2.注意内部类可以直接访问外部类中的static成员,不需要外部类的对象 / 类名。

cpp 复制代码
include<iostream>
using namespace std;
class A
{
private:
	static int _k;
	int _h = 1;
public:
	class B // B默认就是A的友元
	{
	public:
			void foo(const A & a)
		{
			cout << _k << endl; //OK 直接访问static成员
			cout << a._h << endl; //OK
		}
	};
};

//static成员在外部定义初始化
int A::_k = 1;

int main()
{
	//B是一个独立的类,只是外部类类域的限制
	A::B b;
	A aa;
	b.foo(aa);
	return 0;
}

3.外部类的大小和内部类没有关系。

cpp 复制代码
class A
{
private:
	int _n;
	int _m;
public:
	class B // B是A的友元
	{
	public:
		int _a;
	};
};
int main()
{
	A a;
	cout << sizeof(a) << endl;
	return 0;
}

6.匿名对象

用类型(实参) 定义出来的对象叫做匿名对象,想必之前我们定义的 类型 对象名(实参) 定义出来的叫有名对象。
匿名对象⽣命周期只在当前一行,一般临时定义一个对象当前一下下即可,就可以定义匿名对象。

cpp 复制代码
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		cout << "Date" << endl;
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& d)
	{
		cout << "Date(const Date& d)" << endl;
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
	~Date()
	{
		cout << "~Date()" << endl;
		_year = _month = _day = 0;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date();//匿名对象
	return 0;
}

要延长匿名对象的生命周期,一种常见的方法是将其绑定在引用上。

cpp 复制代码
int main()
{
	const Date& dc = Date();//匿名对象也具有常性
	return 0;
}
相关推荐
捕鲸叉4 小时前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer4 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq4 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
青花瓷5 小时前
C++__XCode工程中Debug版本库向Release版本库的切换
c++·xcode
幺零九零零7 小时前
【C++】socket套接字编程
linux·服务器·网络·c++
捕鲸叉7 小时前
MVC(Model-View-Controller)模式概述
开发语言·c++·设计模式
Dola_Pan8 小时前
C++算法和竞赛:哈希算法、动态规划DP算法、贪心算法、博弈算法
c++·算法·哈希算法
yanlou2338 小时前
KMP算法,next数组详解(c++)
开发语言·c++·kmp算法
小林熬夜学编程8 小时前
【Linux系统编程】第四十一弹---线程深度解析:从地址空间到多线程实践
linux·c语言·开发语言·c++·算法
阿洵Rain8 小时前
【C++】哈希
数据结构·c++·算法·list·哈希算法