C++类和对象

2022年9月3日 | 分类: 【概念】

原文:https://blog.csdn.net/IamGreeHand/article/details/120506175

面向过程和面向对象初步认识

C语言:面向过程,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
C++:面向对象,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。

类的引入

C语言:结构体内只能定义变量。
C++:结构体内可以定义变量,也可以定义函数。

struct Student
{
	void SetStudentInfo(const char* name, const char* gender, int age)
	{
		strcpy(_name, name);
		strcpy(_gender, gender);
		_age = age;
	}

	void PrintStudentInfo()
	{
		cout << _name << " " << _gender << " " << _age << endl;
	}

	char _name[20];
	char _gender[3];
	int _age;
};

上面结构体的定义,在C++中更喜欢用class来代替

类的定义

class className
{
 // 类体:由成员函数和成员变量组成
 
}; // 一定要注意后面的分号

class:定义类的关键字
ClassName:类的名字
{}中:类的主体,注意类定义结束时后面分号。

类中的元素:称为类的成员;
类中的函数:称为类的方法或者成员函数;
类中的数据:称为类的属性或者成员变量。

类的两种定义方法

1. 声明和定义全部放在类体中

需要注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。

class Student
{
	void SetStudentInfo(const char* name, const char* gender, int age)
	{
		strcpy(_name, name);
		strcpy(_gender, gender);
		_age = age;
	}

	void PrintStudentInfo()
	{
		cout << _name << " " << _gender << " " << _age << endl;
	}

	char _name[20];
	char _gender[3];
	int _age;
};

2. 声明放在.h文件中,类的定义放在.cpp文件中

//person.h
class Person
{
public:
   //显示信息
   void show();
public:
   char* _name;
   char* _sex;
   int _age;
}

//person.cpp
#include"person.h>

void Person::show()
{
   cout<<_name<<"  "<<_sex<<"  "<<_age<<endl;
} 

注意:一般情况下我们采用第二种方式

类的访问限定符及封装

访问限定符

C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。

访问限定符
┣ public(公有)
┣ protected(保护)
┗ private(私有)

【访问限定符说明】
1. public修饰的成员在类外可以直接被访问
2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
4. class的默认访问权限为private,struct为public(因为struct要兼容C)

封装

在类和对象阶段,我们只研究类的封装特性。

封装是一种管理:
使用类将数据和方法都封装起来。
不想对外开放的就用 protected/private 封装起来,
用 public 封装的成员允许外界对其进行合理的访问。

类的作用域

类定义了一个新的作用域,类的所有成员都在类的作用域中。
在类体外定义成员,需要使用 :: 作用域解析符指明成员属于哪个类域。

class Person
{
public:
 void PrintPersonInfo();
private:
 char _name[20];
 char _gender[3];
 int _age;
};
// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
 cout<<_name<<" "_gender<<" "<<_age<<endl; 
}

类的实例化

用类类型创建对象的过程,称为类的实例化

1. 类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。
2. 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量。
3. 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间。

class Person
{
public:
 void PrintPersonInfo();
private:
 char _name[20];
 char _gender[3];
 int _age;
};

void test()
{
  Person man;   //类的实例化
  man._name="hehe";
  man._age="66";
  man._sex="男";
  man._PrintPersonInfo();
}

类对象模型

如何计算类对象的大小?

class A {
public:
 void PrintA()
 {
 cout<<_a<<endl;
 }
private:
 char _a;
};

那么问题来了?类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小? 想要知道这个,首先我们要弄明白类在内存中的存储方式。

类对象的存储方式

为什么内存要这样存储类了?

原因:每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。

结论:一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类。

如果有小伙伴不怎么明白内存对齐:可以看看这篇文章:自定义类型的知识点
https://blog.csdn.net/IamGreeHand/article/details/119637097

this 指针

this指针的引出

定义一个日期类Date

class Date
{ 
public :
 void Display ()
 {
 cout <<_year<< "-" <<_month << "-"<< _day <<endl;
 }
 
 void SetDate(int year , int month , int day)
 {
 _year = year;
 _month = month;
 _day = day;
 }
private :
 int _year ; // 年
 int _month ; // 月
 int _day ; // 日
};

int main()
{
 Date d1, d2;
 d1.SetDate(2018,5,1);
 d2.SetDate(2018,7,1);
 d1.Display();
 d2.Display();
 return 0; 
 }

对于上述类,有这样的一个问题:Date类中有SetDate与Display两个成员函数,函数体中没有关于不同对象的区分,那当d1调用SetDate函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?

C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。

this指针的特性

1. this指针的类型:类类型* const
2. 只能在“成员函数”的内部使用
3. this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
4. this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递

注意:this指针不能为空

下面来看一个例子:

这里为什么会报错了?首先这个p是一个空指针,但是并不是对象是空指针就一定报错,这里其实更重要的一个原因是PrintA里面为this->_a你对p进行了访问,而空指针是不能访问的。下面我们再来来p->Show()会不会报错?

类的6个默认成员函数

如果一个类中什么成员都没有,称其为空类。

但是空类中真的什么都没有吗?其实不然,任何一个类,即使什么都不写,类中也会自动生成6个默认成员函数。

class Date {}; //空类

构造函数

class Date
{
public:
	Date(int year = 0, 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;
};

例如,上述日期类中的成员函数Date就是一个构造函数。当你用该日期类创建一个对象时,编译器会自动调用该构造函数对新创建的变量进行初始化。

注意:构造函数的主要任务并不是开空间创建对象,而是初始化对象。

构造函数的特性

1. 函数名与类名相同。

2. 无返回值。

3. 对象实例化时编译器自动调用对应的构造函数。

4. 构造函数可以重载。

class Date
{
public:
     // 1.无参构造函数
     Date ()
     {}
	Date(int year = 0, 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;
};

void TestDate()
{
 Date d1; // 调用无参构造函数
 Date d2 (2015, 1, 1); // 调用带参的构造函数
 
 // 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
 // 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
 Date d3(); 
}

5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

class Date
{
public:
 /*
 // 如果用户显式定义了构造函数,编译器将不再生成
 Date (int year, int month, int day)
 {
 _year = year;
 _month = month;
 _day = day;
 }
 */
private:
 int _year;
 int _month;
 int _day;
};

void Test()
{
  // 没有定义构造函数,对象也可以创建成功,因此此处调用的是编译器生成的默认构造函数
   Date d; 
}

6. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。

// 默认构造函数
class Date
{ 
public:
 Date()
 {
 _year = 1900 ;
 _month = 1 ;
 _day = 1;
 }
 
 Date (int year = 1900, int month = 1, int day = 1)
 {
 _year = year;
 _month = month;
 _day = day;
 }
private :
 int _year ;
 int _month ;
 int _day ;
};
// 以下测试函数能通过编译吗?
void Test()
{
 Date d1; 
}

显然这儿是过不了的,因为类中有多个默认函数。

7. 编译器对内置类型使用默认构造函数时,对其成员赋的是随机值。但对自定义类型,会调用它的默认函数。

这儿并没有自己写的构造函数,所以编译时会调用默认的构造函数,又由于类成员都是内置类型,因此赋的都是随机值。

下面再来看看自定义类型。

注意:如果你Time类中没有自己写构造函数,用编译器默认的构造函数,它也是一样会输入随机值的。

析构函数

前面通过构造函数的学习,我们知道一个对象时怎么来的,那一个对象又是怎么没呢的?

析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。

特性:

1. 析构函数名是在类名前加上字符 ~。

2. 无参数无返回值。

3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。

编译器自动生成的析构函数机制:
 1、编译器自动生成的析构函数对内置类型不做处理。
 2、对于自定义类型,编译器会再去调用它们自己的默认析构函数。

4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。

5. 先构造的后析构,后构造的先析构。

拷贝构造函数

构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象,创建新对象时由编译器自动调用。

特性:

1. 拷贝构造函数是构造函数的一个重载形式。

2. 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。

#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 0, 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;
};
int main()
{
	Date d1(2021, 9, 27);
	Date d2(d1); // 用已存在的对象d1创建对象d2

	return 0;
}

因此通过形参不写成引用的形式,会形成无限递归。

3. 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

一般涉及到堆区的问题,浅拷贝是无法解决问题的。下面举个例子:

class Stack
{
public:
	Stack(int capacity = 4)
	{
		_ps = (int*)malloc(sizeof(int)* capacity);
		_size = 0;
		_capacity = capacity;
	}
	void Print()
	{
		cout << _ps << endl;// 打印栈空间地址
	}
private:
	int* _ps;
	int _size;
	int _capacity;
};

int main()
{
	Stack s1;
	s1.Print();// 打印s1栈空间的地址
	Stack s2(s1);// 用已存在的对象s1创建对象s2
	s2.Print();// 打印s2栈空间的地址
	return 0;
}

我们可以看到,类中没有自己定义拷贝构造函数,那么当我们用已存在的对象来创建另一个对象时,将调用编译器自动生成的拷贝构造函数。这段代码中,我们的本意是用已存在的对象s1创建对象s2,但编译器自动生成的拷贝构造函数,完成的是浅拷贝,拷贝出来的对象s2将不能满足我们的要求。

结果打印s1栈和s2栈空间的地址相同,这就意味着,就算在创建完s2栈后,我们对s1栈做的任何操作都会直接影响到s2栈。

这个时候问题就很严重了。首先我们对s1的修改都会直接影响s2,而且更重要的一个是:我们对它们共同指向的那块空间进行了两次的析构,会造成空间多次释放的问题。

运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

函数名字为:关键字operator后面接需要重载的运算符符号。

函数原型:返回值类型 operator操作符(参数列表)

注意:
1.不能通过连接其他符号来创建新的操作符:比如operator@
2.重载操作符必须有一个类类型或者枚举类型的操作数
3.用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义
4.作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参
5.* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。

==运算符重载

bool operator==(const Date& d1, const Date& d2) {
 return d1._year == d2._year;
 && d1._month == d2._month
 && d1._day == d2._day; }

对于这个重载的函数,你可以定义再类里面,这样就少一个参数,因为有this指针的存在。你也可以定义在外面,但是定义在外面时,可能你的类成员时private封装的,无法访问到,这时有两个解决办法:一是把类成员用public封装,二是用友元函数(之后会讲到)。

= 运算符重载

	Date& operator=(const Date& d)// 赋值运算符重载函数
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}

这里为什么要返回引用了?如果你去测试发现D1=D2,如果你的返回值是Date的话,似乎也能过,但是如果你的测试用例是D1=D2=D3的话,那就一定过不了了,因为你不是返回的对象本身,无法形成链式编程,这也是为什么这儿返回*this的原因,因为this是指向左操作符的。

其他一些运算符的重载这儿就不多说了,有兴趣的小伙伴可以自己去尝试尝试。下面来说几个重载运算符时的注意点。

重载赋值运算符需要注意以下几点:

一、参数类型设置为引用,并用const进行修饰

赋值运算符重载函数的第一个形参默认是this指针,第二个形参是我们赋值运算符的右操作数。
由于是自定义类型传参,我们若是使用传值传参,会额外调用一次拷贝构造函数,所以函数的第二个参数最好使用引用传参(第一个参数是默认的this指针,我们管不了)。
其次,第二个参数,即赋值运算符的右操作数,我们在函数体内不会对其进行修改,所以最好加上const进行修饰。

二、返回值使用引用返回

原因在=运算符重载中说过了,为了返回对象自身,形成链式编程。(return *this才是返回自身,不要忘记解引用哦)

三、一个类如果没有显示定义赋值运算符重载,编译器也会自动生成一个,完成对象按字节序的值拷贝

没错,赋值运算符重载编译器也可以自动生成,并且也是支持连续赋值的。但是编译器自动生成的赋值运算符重载完成的是对象按字节序的值拷贝,例如d2 = d1,编译器会将d1所占内存空间的值完完全全地拷贝到d2的内存空间中去,类似于memcpy。

但是有些类就不行了,所以有些类还是要我们自己写赋值运算符重载的。

注意区分拷贝和赋值:

	Date d1(2021, 6, 1);
	Date d2(d1);
	Date d3 = d1;

这里一个三句代码,我们现在都知道第二句代码调用的是拷贝构造函数,那么第三句代码呢?调用的是哪一个函数?是赋值运算符重载函数吗?

其实第三句代码调用的也是拷贝构造函数,注意区分拷贝构造函数赋值运算符重载函数的使用场景:
拷贝构造函数:用一个已经存在的对象去构造初始化另一个即将创建的对象。
赋值运算符重载函数:在两个对象都已经存在的情况下,将一个对象赋值给另一个对象。

const修饰成员函数

将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰的是类成员函数隐含的this指针,表明在该成员函数中不能对this指针指向的对象进行修改。

例如,我们可以对类成员函数中的打印函数进行const修饰,避免在函数体内不小心修改了对象:

	void Print()const// cosnt修饰的打印函数
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

注意:在使用const时要注意,权限不能放大,但是可以缩小。

再谈构造函数

构造函数体赋值

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

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

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

class Date
{
public:
	// 构造函数
	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;// 第一次赋值
		_year = 2022;// 第二次赋值
		//...
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

初始化列表

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

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

注意事项
1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2. 类中包含以下成员,必须放在初始化列表进行初始化:

A.引用成员变量

引用类型的变量在定义时就必须给其一个初始值,所以引用成员变量必须使用初始化列表对其进行初始化。

	int a = 10;
	int& b = a;// 创建时就初始化

B.const成员变量

被const修饰的变量也必须在定义时就给其一个初始值,也必须使用初始化列表进行初始化

	const int a = 10;//correct 创建时就初始化
	const int b;//error 创建时未初始化

C. 自定义类型成员(该类没有默认构造函数)

若一个类没有默认构造函数,那么我们在实例化该类对象时就需要传参对其进行初始化,所以实例化没有默认构造函数的类对象时必须使用初始化列表对其进行初始化。

在这里再声明一下,默认构造函数是指不用传参就可以调用的构造函数:
 1.我们不写,编译器自动生成的构造函数。
 2.无参的构造函数。
 3.全缺省的构造函数。

class A //该类没有默认构造函数 
{
public:
	A(int val) //注:这个不叫默认构造函数(需要传参调用)
	{
		_val = val;
	}
private:
	int _val;
};

class B
{
public:
	B()
		:_a(2021) //必须使用初始化列表对其进行初始化
	{}
private:
	A _a; //自定义类型成员(该类没有默认构造函数)
};

总结一下:在定义时就必须进行初始化的变量类型,就必须放在初始化列表进行初始化。

3. 尽量使用初始化列表初始化

因为初始化列表实际上就是当你实例化一个对象时,该对象的成员变量定义的地方,所以无论你是否使用初始化列表,都会走这么一个过程(成员变量需要定义出来)。

严格来说:

1. 对于内置类型,使用初始化列表和在构造函数体内进行初始化实际上是没有差别的,其差别就类似于如下代码:

// 使用初始化列表
int a = 10
// 在构造函数体内初始化(不使用初始化列表)
int a;
a = 10;

2. 对于自定义类型,使用初始化列表可以提高代码的效率

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

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

我们若是想在不使用初始化列表的情况下,达到我们想要的效果,就不得不这样写了:

class Time
{
public:
	Time(int hour = 0)
	{
		_hour = hour;
	}
private:
	int _hour;
};
class Test
{
public:
	// 在构造函数体内初始化(不使用初始化列表)
	Test(int hour)
	{
		Time t(hour);// 调用一次Time类的构造函数
		_t = t;// 调用一次Time类的赋值运算符重载函数
	}
private:
	Time _t;
};

这时如果我们要实例化一个Test类的对象,在实例化过程中先调用了一次Time类的构造函数,又调用了一次Time类的赋值运算符重载函数,效率就降下来了。

4. 成员变量在类中声明的次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后顺序无关

举个例子:

#include <iostream>
using namespace std;
int i = 0;
class Test
{
public:
	Test()
		:_b(i++)
		,_a(i++)
	{}
	void Print()
	{
		cout << "_a:" << _a << endl;
		cout << "_b:" << _b << endl;
	}
private:
	int _a;
	int _b;
};
int main()
{
	Test test;
	test.Print(); //打印结果test._a为0,test._b为1
	return 0;
}

代码中,Test类构造函数的初始化列表中成员变量_b先初始化,成员变量_a后初始化,按道理打印结果test._a为1,test._b为0,但是初始化列表的初始化顺序是成员变量在类中声明次序,所以最终test._a为0,test._b为1。

explicit关键字

构造函数不仅可以构造和初始化对象,对于单个参数的构造函数,还支持隐式类型转换。

#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 0) //单个参数的构造函数
		:_year(year)
	{}
	void Print()
	{
		cout << _year << endl;
	}
private:
	int _year;
};
int main()
{
	Date d1 = 2021; //支持该操作
	d1.Print();
	return 0;
}

在语法上,代码中Date d1 = 2021等价于以下两句代码:

Date tmp(2021); //先构造
Date d1(tmp); //再拷贝构造

在早期的编译器中,当编译器遇到Date d1 = 2021这句代码时,会先构造一个临时对象,再用临时对象拷贝构造d1;但是现在的编译器已经做了优化,当遇到Date d1 = 2021这句代码时,会按照Date d1(2021)这句代码处理,这就叫做隐式类型转换。

实际上,我们早就接触了隐式类型转换,只是我们不知道而已,以下代码也叫隐式类型转换:

int a = 10;
double b = a; //隐式类型转换

在这个过程中,编译器会先构建一个double类型的临时变量接收a的值,然后再将该临时变量的值赋值给b。这就是为什么函数可以返回局部变量的值,因为当函数被销毁后,虽然作为返回值的变量也被销毁了,但是隐式类型转换过程中所产生的临时变量并没有被销毁,所以该值仍然存在。

但是,对于单参数的自定义类型来说,Date d1 = 2021这种代码的可读性不是很好,我们若是想禁止单参数构造函数的隐式转换,可以用关键字explicit来修饰构造函数。

static成员

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

特性

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

#include <iostream>
using namespace std;
class Test
{
private:
	static int _n;
};
int main()
{
	cout << sizeof(Test) << endl;
	return 0;
}

结果计算Test类的大小为1,因为静态成员_n是存储在静态区的,属于整个类,也属于类的所有对象。所以计算类的大小或是类对象的大小时,静态成员并不计入其总大小之和。

2. 静态成员变量必须在类外定义,定义时不添加static关键字

class Test
{
private:
	static int _n;
};
// 静态成员变量的定义初始化
int Test::_n = 0;

注意:这里静态成员变量_n虽然是私有,但是我们在类外突破类域直接对其进行了访问。这是一个特例,不受访问限定符的限制,否则就没办法对静态成员变量进行定义和初始化了

3. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员

class Test
{
public:
	static void Fun()
	{
		cout << _a << endl; //error不能访问非静态成员
		cout << _n << endl; //correct
	}
private:
	int _a; //非静态成员
	static int _n; //静态成员
};

注意:含有静态成员变量的类,一般含有一个静态成员函数,用于访问静态成员变量

4. 访问静态成员变量的方法

一. 当静态成员变量为公有时,有以下几种访问方式:

#include <iostream>
using namespace std;
class Test
{
public:
	static int _n; //公有
};
// 静态成员变量的定义初始化
int Test::_n = 0;
int main()
{
	Test test;
	cout << test._n << endl; //1.通过类对象突破类域进行访问
	cout << Test()._n << endl; //3.通过匿名对象突破类域进行访问
	cout << Test::_n << endl; //2.通过类名突破类域进行访问
	return 0;
}

二. 当静态成员变量为私有时,有以下几种访问方式:

#include <iostream>
using namespace std;
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.通过匿名对象调用成员函数进行访问
	cout << Test::GetN() << endl; //3.通过类名调用静态成员函数进行访问
	return 0;
}

5. 静态成员和类的普通成员一样,也有public、private和protected这三种访问级别

所以当静态成员变量设置为private时,尽管我们突破了类域,也不能对其进行访问。

注意区分两个问题:
 1、静态成员函数可以调用非静态成员函数吗?
 2、非静态成员函数可以调用静态成员函数吗?
问题1:不可以。因为非静态成员函数的第一个形参默认为this指针,而静态成员函数中没有this指针,故静态成员函数不可调用非静态成员函数。
问题2:可以。因为静态成员函数和非静态成员函数都在类中,在类中不受访问限定符的限制。

C++11中成员初始化的新玩法

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

class A
{
public:
	void Print()
	{
		cout << _a << endl;
		cout << _p << endl;
	}
private:
	// 非静态成员变量,可以在成员声明时给缺省值。
	int _a = 10; 
	int* _p = (int*)malloc(4);
	static int _n; //静态成员变量不能给缺省值
};

初始化列表是成员变量定义初始化的地方,你若是给定了值,就用你所给的值对成员变量进行初始化,你若没有给定值,则用缺省值进行初始化,若是没有缺省值,则内置类型的成员就是随机值。

友元

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

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

友元函数

这里就以我们之前没有实现的一个重载来举例,我们来实现一下<<的重载。

这里要实现首先要明白一个问题:

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

class Date
{
	// 友元函数的声明
	friend ostream& operator<<(ostream& out, const Date& d);
	friend istream& operator>>(istream& in, Date& d);
public:
	Date(int year = 0, int month = 1, int day = 1)
	{
		_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;
}
// >>运算符重载
istream& operator>>(istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	return in;
}

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

友元类

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

class A
{
	// 声明B是A的友元类  可以理解为B是A的好朋友,A的任何东西B都可以访问,包括私有类型的。
	friend class B;
public:
	A(int n = 0)
		:_n(n)
	{}
private:
	int _n;
};
class B
{
public:
	void Test(A& a)
	{
		// B类可以直接访问A类中的私有成员变量  
		cout << a._n << endl;
	}
};

友元类说明:
1. 友元关系是单向的,不具有交换性。

例如上述代码中,B是A的友元,所以在B类中可以直接访问A类的私有成员变量,但是在A类中不能访问B类中的私有成员变量。

2. 友元关系不能传递。

如果A是B的友元,B是C的友元,不能推出A是C的友元。

内部类

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

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

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

class A {
public:
	A(int k,int h)
	{ 
		this->_k = k;
		this->_h = h;
	}
private:
	static int _k;
	int _h;
public:
	class B
	{
	public:
		void foo(const A& a)
		{
			cout <<_k << endl;//OK  a._k也ok
			cout << a._h << endl;//OK
		}
	};
};

int A::_k = 1;

int main()
{
	A::B b;
	b.foo(A(2,10));

	return 0;
}

再次理解封装

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

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

再次理解面向对象

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