C++类和对象(下)

文章目录

const成员函数

概念:将const修饰的成员函数称之为const成员函数

定义:const修饰成员函数时,放到成员函数参数列表的后面

例:

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

一般的,用普通对象来调用成员函数时,通过对象的地址传给隐含的this指针来访问成员变量,此时的对象d1是被const修饰的对象,当调用普通的Print函数时,是调不动的,因为这里出现了权限放大的现象,此时取d1的地址时,它的类型是const Date*,该const修饰指针所指向的内容,而Print函数的this指针的类型是Date* ,当把d1的地址传过去时,出现了权限放大的现象,所以就出现了const修饰this指针,它的类型就变成了const Date*

cpp 复制代码
void Print()const

板图如下:

权限放大和缩小的总结:
const用于指针时,情况一,const位于星号之前,修饰指针所指向的内容,存在权限放大或缩小的情况;情况二,const位于星号之后,修饰指针本身,不存在权限放大和缩小的情况

const修饰成员函数,本质上修饰的是隐含的this指针,使该成员函数不能修改类中任何成员变量,普通的对象可以调用const成员函数,毕竟权限是可以缩小的,const对象也可以调用const函数,所以,不需要修改成员函数的成员函数都建议加上const

注:const在声明和定义时都要加上

取地址运算符重载

取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,代码如下:

cpp 复制代码
class Date
{
public:
	Date(int year = 1990, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//普通取地址运算符重载
	Date* operator&(const Date& d)
	{
		return this;
	}
	//const取地址运算符重载
	const Date* operator&(const Date* d)const
	{
		return this;
	}
private:
	int _year;
	int _month;
	int _day;
};

一般情况下,取地址运算符重载编译器默认生成的就用了,不需要自己实现,除非有特殊要求,如:不想要别人取到地址,代码如下:

cpp 复制代码
Date* operator&(const Date& d)
{
	return (Date*)nullptr;
	//或者给个假地址
	// return (Date*)0x11223fff;
}

初始化列表

定义:初始化列表是一种构造函数的初始化方式

初始化列表的格式:

初始化列表在构造函数的函数体外实现,以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始化值或者表达式

例:

cpp 复制代码
class Date
{
public:
	Date(int year,int month,int day)
		//初始化列表
		:_year(year)
		,_month(month)
		,_day(day)
	{
		//函数体
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	return 0;
}

初始化列表的特点:

  • 每个成员变量在初始化列表中只能出现一次,且每个成员变量都会走初始化列表,就算不在初始化列表中的成员变量也会走
cpp 复制代码
class Time
{
public:
	Time(int hour = 10)
		:_hour(hour)
	{
		cout << "Time()" << endl;
	}
private:
	int _hour;
};
class Date
{
public:
	Date(int year = 1,int month = 1,int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
		//_t没有在初始化列表中显示也会初始化列表
	{}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};
int main()
{
	Date d1;
	return 0;
}

可以看到_t 这个成员变量也走了初始化列表去调用默认构造函数,所以尽量使用初始化列表,因为那些不在初始化列表初始化的成员变量也会走初始化列表

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

大家可能会这么想,明明可以直接在函数体里使用赋值的方式进行初始化,为什么还需要初始化列表呢,因为有一些成员变量是必须要用初始化列表进行初始化的,我们来看下面这段代码(两个栈实现一个队列的部分代码):

cpp 复制代码
class stack
{
public:
	stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			perror("malloc fail!");
			exit(-1);
		}
		_capacity = capacity;
		_top = 0;
	}
	~stack()
	{
		free(_a);
		_a = nullptr;
		_capacity = _top = 0;
	}
private:
	int* _a;
	int _capacity;
	int _top;
};
class Myqueue
{
private:
	stack _popst;
	stack _pushst;
};
int main()
{
	Myqueue mq1;
	return 0;
}

对于自定义类型,编译器会自动调用它的默认构造函数,所以队列类不需要我们定义构造函数,但是,若将代码一改,将栈的构造函数的参数的缺省值去掉,会出现什么问题呢

上面图片中的报错说明,编译器找不到Myqueue的默认构造函数,无法对mq1进行初始化,要解决这样的问题,那就得显示定义Myqueue的默认构造,但是不能在默认构造的函数体中进行初始化,因为在函数体中调用不了自定义类型成员对应的构造函数,C++规定只能在初始化列表中才能调用自定义类型成员的构造函数,代码如下:

cpp 复制代码
class stack
{
public:
	stack(int capacity)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			perror("malloc fail!");
			exit(-1);
		}
		_capacity = capacity;
		_top = 0;
	}
	~stack()
	{
		free(_a);
		_a = nullptr;
		_capacity = _top = 0;
	}
private:
	int* _a;
	int _capacity;
	int _top;
};
class Myqueue
{
public:
	Myqueue(int n = 10)
		:_popst(n)
		,_pushst(n) // 会去调用栈的构造函数
	{}
private:
	stack _popst;
	stack _pushst;
};
int main()
{
	Myqueue mq1;
	return 0;
}

除了没有默认构造类类型成员变量,还有引用成员变量,const成员变量,这三种成员变量必须要用初始化列表进行初始化,若在函数体内初始化,编译器就会报错

根据初始化列表的特点,每个成员变量只能在初始化中出现一次,且每个成员变量都会走初始化列表,可以认为初始化列表是每个成员变量定义初始化的地方,根据上面三种成员变量在定义时就必须初始化且被const和引用修饰的成员变量一旦初始化就不可修改的特征,就必须使用初始化列表初始化,因为在函数体中成员变量可出现多次,编译器不知道哪一条语句才是成员变量要初始化的语句

  • C++支持在成员变量声明时给缺省值

看下面代码:

cpp 复制代码
class Date
{
public:
	Date(int year = 2,int month = 2,int day = 2)
		:_year(year)
		,_month(month)
		// _day不在初始化列表中
	{}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	return 0;
}

_day 似乎没走初始化列表,编译器是这样的,对于自定义类型它会调用对应的默认构造,不在初始化列表中也会去调用,但若是调用的默认构造需要传参时,不在初始化列表中给值是不能成功调用的;对于内置类型不在初始化列表中也会走初始化列表,但上面的情况似乎没有被初始化,为了自定义类型能成功地调用默认构造和内置类型成功的在初始化列表中被初始化,C++补充了条语法,就是支持在成员变量声明时给缺省值,作用是给那些不在初始化列表的成员变量使用,若在初始化列表中显示了,就不会用到缺省值,代码如下:

cpp 复制代码
class Time
{
public:
	Time(int hour)
		:_hour(hour)
	{
		cout << "Time()" << endl;
	}
private:
	int _hour;
};
class Date
{
public:
	Date(int year = 2,int month = 2,int day = 2)
		:_year(year)
		,_month(month)
	{}
private:
	int _year;
	int _month;
	int _day = 1;
	Time _t = 5;
};
int main()
{
	Date d1;
	return 0;
}

有了缺省值,就可保证每个成员变量都会被初始化了

总结: 成员变量走初始化列表的逻辑


注意:初始化列表的初始化顺序与成员变量声明的顺序一致

cpp 复制代码
class A
{
public:
	A(int a = 1)
		:_a1(a)
		,_a2(_a1)
	{}
	void Print()
	{
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2;
	int _a1;
};
int main()
{
	A aa;
	aa.Print();
	return 0;
}

可看到,_a2 先被初始化,但这时的_a1 还是个随机值,所以出来的结果就是_a2 是随机值

类型转换

  • C++支持内置类型隐式转换成类类型,需要有相关内置类型为参数的构造函数即可
cpp 复制代码
class A
{
public:
	A(int aa = 0) //内置类型为参数的构造函数
		:_a1(aa)
	{}
	A(int a1,int a2)
		:_a1(a1)
		,_a2(a2)
	{}
	void Print()
	{
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a1 = 0;
	int _a2 = 1;
};
int main()
{
	A aa1 = 1;
	aa1.Print();
	return 0;
}

可看到,内置类型的1 隐式类型转换给了类A中的_a1。这是怎么回事呢,本质上其实是1构造了一个A的临时对象,再用这个临时对象拷贝构造给aa1。但是编译器在这里做了个简单的优化,将构造一个临时对象再拷贝构造给aa1,优化成直接构造aa1,板图如下:

证明隐式类型转换时会产生临时对象:

cpp 复制代码
const A& aa2 = 1;

这里引用了临时对象,临时对象具有常性,所以用const修饰。在这里编译器就不会优化了,若优化了这里的引用就变成了野引用了

  • 关键字explicit

若不想隐式类型转换发生就可在构造函数前面加上explicit

  • 多参数的隐式类型转换
cpp 复制代码
A aa1 = {3,3}; // C++11才支持
  • 类型转换的意义
cpp 复制代码
class A
{
public:
	A(int aa = 0)
		:_a1(aa)
	{}
	A(int a1,int a2)
		:_a1(a1)
		,_a2(a2)
	{}
	void Print()
	{
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a1 = 0;
	int _a2 = 1;
};
class stack //用来存储A类型的栈
{
public:
	void Push(const A& aa)
	{
		//...
	}
};
int main()
{
	// 不用隐式类型转换
	stack st1;
	A aa1;
	st.Push(aa1);
	
	// 直接隐式类型转换传参
	stack st2;
	st.Push(5);
	st.Push({6,6}); 
	return 0;
}

隐式类型转换在传值传参写起来更方便等等

自定义类型转换为自定义类型可以转换吗?

借助有要转换的自定义类型的参数的构造函数即可

cpp 复制代码
class A
{
public:
	A(int aa = 0)
		:_a1(aa)
	{}
	A(int a1, int a2)
		:_a1(a1)
		, _a2(a2)
	{}
	void Print()
	{
		cout << _a1 << " " << _a2 << endl;
	}
	int Get()const
	{
		return _a1 + _a2;
	}
private:
	int _a1 = 0;
	int _a2 = 1;
};
class B
{
public:
	B(const A& aa) // A类转换成B类,有A类类型的参数的构造函数
		:_b(aa.Get())
	{}
	void Print()
	{
		cout << _b << endl;
	}
private:
	int _b = 0;
};
int main()
{
	A aa1;
	B bb1 = aa1; // A隐式类型转换成了B
	// 同样构造aa1构造了一个B类的临时对象
	bb1.Print();
	return 0;
}

正是因为构造函数的参数中有A类型作为参数,才可成功隐式转换

总结:两个类型有一定的关联才能转换

static成员

  1. static成员变量

用stati修饰成员变量,称之为静态成员变量。静态成员变量一定要在类外进行初始化,它为所有对象共享,不属于某个类对象的专属,存放在静态区,通俗来讲就是,一个全局变量,被放到了类里面,受类的限制,变成了类的专属

  • 使用方法

举个例子:统计A类创建了多少个对象?

cpp 复制代码
#include <iostream>
using namespace std;
class A
{
public:
	A()
	{
		++_count;
	}
	A(const A& aa)
	{
		++_count;
	}
//private:
	static int _count; // 类里面声明
};
int A::_count = 0; // 类外定义
void func(A aa)
{

}
int main()
{
	A aa1;
	A aa2 = aa1;
	func(aa1);
	// _count突破访问限定符时的访问方式有两种
	// static成员变量属于整个类和所有对象
	cout << A::_count << endl; 
	cout << aa1._count << endl; //不代表_count属于aa1
	return 0;
}

一共创建了3个对象

  • 计算aa1和aa2的大小

计算的结果是1而不是4说明了静态成员变量不属于某个对象的专属,是1的原因是编译器为aa1和aa2开辟的一个空间

  1. static成员函数

在前面统计类创建了多少个对象的例子中,访问静态成员变量时,我们将其设为了公有,一般情况下,成员变量是不允许在类外直接访问的,所以想要不突破限定符的限制来访问静态成员变量,可以通过静态成员函数来进行访问

cpp 复制代码
class A
{
public:
	A()
	{
		++_count;
	}
	A(const A& aa)
	{
		++_count;
	}
	static int Get_count() // 静态成员函数
	{
		return _count;
	}
private:
	static int _count; // 类里面声明
};
int A::_count = 0; // 类外定义
void func(A aa)
{}
int main()
{
	A aa1;
	A aa2 = aa1;
	cout << A::Get_count() << endl;
	return 0;
}

像上面这样就可以访问静态成员变量了,但问题来了,为什么静态成员函数是这样调用,而不是下面这样的方式调用呢?

cpp 复制代码
cout << aa1.Get_count() << endl;

这就与静态成员函数的特性有关了。我们知道,成员函数是有一个隐含的this指针的,选择用对象.成员函数的方式调用成员函数是为了传对象的地址给this指针,而上面的代码中调用静态成员函数时不需要用对象来进行调用,这就说明了,静态成员函数没有隐含的this指针,由于受到类的限制,所以调用时要用类来进行调用

  • 静态成员的其他特性
  • 静态成员函数只可访问静态成员变量,不能访问非静态成员变量,因为没有隐含的this指针,但非静态成员函数两者皆可访问
  • 静态成员也是类的成员,受类的限制
  • 静态成员变量在声明时不可以给缺省值,因为缺省值是给初始化列表准备的,静态成员变量不属于某个对象,不走构造函数进行初始化

友元

概念:友元是一种突破类访问限定符的方式,分为友元函数和友元类

格式:在函数声明或类声明前面加上friend,并把友元函数声明或友元类声明放到一个类里面

  1. 友元函数

特性:

  • 外部友元可以访问类的私有和保护成员,友元函数是声明不是成员函数
cpp 复制代码
class A
{
	friend int func(A& aa); //友元函数声明
public:
	A(int a1 = 1,int a2 = 2)
		:_a1(a1)
		,_a2(a2)
	{}
private:
	int _a1;
	int _a2;
};
int func(A& aa)
{
	return aa._a1 + aa._a2; // 可访问私有成员变量
}
int main()
{
	A aa1;
	cout << func(aa1) << endl;
	return 0;
}
  • 友元函数可以在类里的任何地方定义,不受类的访问限定符限制
  • 一个函数可以是多个类的友元函数
  1. 友元类

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

cpp 复制代码
class A
{
	friend class B; // 友元类声明
private:
	int _a1 = 1;
	int _a2 = 2;
};
class B
{
public:
	// 友元类中的成员函数
	void func1(const A& aa)
	{
		//直接访问A类中的私有和保护成员
		cout << aa._a1 << " " << _b1 << endl;
	}
	void func2(const A& aa)
	{
		cout << aa._a2 << " " << _b2 << endl;
	}
private:
	int _b1 = 3;
	int _b2 = 4;
};
int main()
{
	A aa1;
	B bb1;
	bb1.func1(aa1);
	bb1.func2(aa1);
	return 0;
}

注:友元是单向且不能传递

内部类

概念:把一个类定义在另一个类里面

例:

cpp 复制代码
class A
{
private:
	int _a = 1;
	static int _i;
public:
	class B // B默认是A的友元类
	{
	public:
		void func(const A& aa)
		{
			// 可访问A的私有和保护成员
			cout << _i << endl;
			cout << aa._a << endl;
			cout << _b << endl;
		}
	private:
		int _b = 1;
	};
};
int A::_i = 1;
int main()
{
	A::B b; // 因为B受到A类的限制,所以要这样访问
	A aa1;
	b.func(aa1);
	return 0;
}


在内部类的条件下,探索两个类之间的关系

可通过计算外部类的大小来确定,如下:

从计算结果看出,A类的大小是4,说明了A中只存储了_a 这个变量,而_i 是静态成员变量,存储在静态区中,所以可以得到,外部类和内部类不是包含关系,而是单独的两个类,只是内部类受到了外部类的类域和访问限定符的限制而已

注:内部类默认是外部类的友元类

匿名对象

概念:用类型(实参)定义出来的对象叫作匿名对象,相比我们之前定义的类型+对象名定义出来的叫作有名对象

例:

cpp 复制代码
class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}
private:
	int _a;
};
int main()
{
	//有名对象
	A aa1(1);
	A aa2; // 有名对象不传参时,不需要加括号
	// 匿名对象
	A(1);
	A(); // 匿名对象不传参时,必须加括号
	return 0;
}

特点: 匿名对象的声明周期只在当前一行,过来定义那一行声明周期结束

cpp 复制代码
class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
};
int main()
{
	A aa1(1);
	A aa2;

	A(1);
	A(2);
	cout << "###################" << endl;
	return 0;
}

看上面的图片,我们知道有名对象的析构函数是在程序运行完之后才调用的,但是我们可以看到匿名对象创建完之后就被析构了,说明匿名对象的声明周期只在当前一行

匿名对象可用于作为函数的缺省值,如下:

cpp 复制代码
void func(A aa = A(10))
{
	// ....
}

匿名对象可以用引用&,如下:

cpp 复制代码
const A& ra = A();

匿名对象与临时对象相似,需要const修饰
注意:此时的匿名对象的周期得到了延长,若不得到延长,一旦结束了匿名对象的生命周期,就会出现野引用的现象

相关推荐
吃海鲜的骆驼3 分钟前
四、JVM原理-4.1、JVM介绍
java·开发语言·jvm·面试
pjx9875 分钟前
JVM 执行引擎详解:理论与实践
开发语言·jvm
白茶等风1213813 分钟前
C#_结构(Struct)详解
开发语言·c#
ephemerals__15 分钟前
【c++】STL简介
开发语言·c++
UestcXiye23 分钟前
Leetcode16. 最接近的三数之和
c++·leetcode·排序·双指针·数据结构与算法
赤橙红的黄44 分钟前
代理模式-动态代理
java·开发语言·代理模式
Au_ust1 小时前
go的结构体、方法、接口
开发语言·golang
-VE-1 小时前
模板初阶(c++)
开发语言·c++
shigen011 小时前
结合HashMap与Java 8的Function和Optional消除ifelse判断
java·开发语言
CN.LG1 小时前
浅谈Python之协程
开发语言·python