C++类与对象
1. 类的引入与定义
1.1 类的引入
在c语言中,学习了结构体,但是此时的结构体中只能定义变量。而在c++中,结构体中不仅可以定义变量,还可以定义函数。
在c++中更常用class来代替结构体。
1.2 类的定义
class Person//Person为类的名字
{
//大括号内为类体,类体由成员函数和成员变量组成。
};//最后一定不要少了分号。
在上述代码中,class为定义类的关键字,Person为类的名字,{}内为类的主体。类中的元素称为类的成员, 类中的数据称为成员标量或者类的属性,类中的函数称为成员函数或者类的方法。
类的定义有两种方式:
- 声明和定义都在类体中
class Person//Person为类的名字
{
public:
void show()
{
cout<<"名字"<<_name<<endl;
}
public:
char _name;
char _sex;
int _age;
};
- 声明在.h文件中,定义在.cpp文件中。
//.h文件中
class Person
{
public:
void show();
public:
char _name;
char _sex;
int _age;
};
//.cpp文件中
#include"Person.h"
void Person::show()
{
cout<<"名字"<<_name<<endl;
}
//一般工程量大的情况下,采用第二种方法较为方便。
1.3 类的访问限定符及封装
1.3.1 类的访问限定符
在class中,访问限定符共有三种:
- public(共有):修饰的成员在类外可以直接被访问
- protected(保护):类外不可直接访问
- private(私有):类外不可直接访问
class和struct不同的是class的默认访问权限为private,struct的默认访问权限是public。
1.3.2 封装
面对对象的三大特性:封装、继承、多态。
封装就是将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
1.4 类的作用域
以第二种类定义的方式来说,当在.h文件中声明类之后,在.cpp文件中定义函数时(类外定义),需要在函数名前加上==::==作用域解析符指明成员属于哪个类。
//.h文件中
class Person
{
public:
void show();
public:
char _name;
char _sex;
int _age;
};
//.cpp文件中
#include"Person.h"
void Person::show()
{
cout<<"名字"<<_name<<endl;
}
1.5 类的实例化
类就相当于模板,模板里边有固定的成员,定义类并没有给其分配实际的内存空间。而当对类进行实例化时,实例化出的对象是占有实际的内存空间的,用来存储类成员变量。(一个类可以实例化出很多个对象)
class Person
{
public:
void show();
private:
char _name;
char _sex;
int _age;
}
int main()
{
Person p1;//Person类实例化出的对象p1
Person p2;//Person类实例化出的对象p2
//.......还可以实例化出无数个对象
return 0;
}
1.6 类对象模型
1.6.1 类对象的大小
在c++中,成员变量和成员函数是分开存储的,只有非静态成员变量才属于类的对象上,而静态成员变量不占用对象空间,成员函数和静态成员函数也不占用对象空间。
#include<iostream>
using namespace std;
class Person
{
public:
Person()
{
_age = 0;
}
int _age;
static char _age2;
void fun()
{
cout << "成员函数" << endl;
}
static void func()
{
cout << "静态成员函数" << endl;
}
};
int main()
{
cout << "Person的大小为" << sizeof(Person) <<endl;
return 0;
}
此程序为计算类大小的程序,运行结果如下:
此结果也可表明,只有非静态成员变量才属于类的对象上。成员函数则会存放在公共的代码段。需要注意的一个问题是内存对齐。
1.6.1.1 结构体内存对齐规则
- 第一个成员在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数=编译器默认的一个对齐数与该成员大小的较小值(vscode默认对齐数为8)。 - 结构体总大小为:最大对齐数(所有变量类型的最大者与默认对齐参数取最小)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
如果定义两个char类型的成员变量以及一个int类型的成员变量,由于vscode默认对其数那么用sizeof计算出的大小为8。
#include<iostream>
using namespace std;
class Person
{
public:
Person()
{
_age = 0;
}
int _age;
char _name;
char _sex;
};
int main()
{
cout << "Person的大小为" << sizeof(Person) <<endl;
return 0;
}
运行结果为:
由此可以看出,编译器在编译时,自动遵循结构体内存对齐规则。
2. this指针
2.1 this指针的引出
先定义一个Person类
#include<iostream>
using namespace std;
#include<string.h>
class Person
{
public:
void display()
{
cout << "姓名:" << _name << "\t" << "年龄:" << _age <<endl;
}
void setperson(string name , int age)
{
_name = name ;
_age = age;
}
private:
string _name;
int _age;
};
int main()
{
Person p1,p2;
p1.setperson("张三",18);
p2.setperson("李四",19);
p1.display();
p2.display();
return 0;
}
在上述程序中,有一个问题。在main函数中,有两个Person的实例化对象p1和p2。当调用类中的setperson函数时,此函数是怎么区分设置p1对象还是设置p2对象。
在c++中是通过引入this指针来解决这一问题的。c++在编译时,会对每一个“非静态成员函数”增加一个隐藏的指针参数(不可以显式定义this指针),让这个指针指向当前的对象。例如:
p1.setperson("张三",18);
//则在此成员函数调用时,this指针会指向p1
此过程是编译器自动完成,不需要用户传递。
2.2 this指针的特性
- this指针的类型:*const
- this指针只能在成员内部使用
- this指针本质上其实是一个成员函数的形参,在对象调用函数时,将对象地址作为实参传递给this形参,所以对象中不存储this指针。
编译器处理成员函数隐含的this指针是以下列方式进行处理:
void display()
{
cout << _name <<endl;
}//这是成员函数
void display(Person *this)
{
cout << this->_name <<endl;
}//这是编译器自动做的处理
面试问题:
- this指针存放在哪里?
答:存放在栈中。 - this指针可以为空吗?
答:this指针可以为空,但是为空的时候,不可以访问成员变量,否则会报错。
3. 类的默认成员函数
3.1 类共有6个默认成员函数
class Person{};
如上代码所示,当定义一个类,但是类内什么都没有,即为空类。空类中什么都没有,但是都会自动生成6个默认成员函数。
- 构造函数:主要完成初始化工作。
- 析构函数:主要完成清理工作。
- 拷贝构造:使用同类对象初始化创建对象。
- 赋值重载:把一个对象赋值给另一个对象。
- 普通对象取地址重载(很少自己实现)
- const对象取地址(很少自己实现)
3.2 构造函数
3.2.1 构造函数的引入
以Person类为例
#include<iostream>
using namespace std;
#include<string.h>
class Person
{
public:
void display()
{
cout << "姓名:" << _name << "\t" << "年龄:" << _age <<endl;
}
void setperson(string name , int age)
{
_name = name ;
_age = age;
}
private:
string _name;
int _age;
};
int main()
{
Person p1,p2;
p1.setperson("张三",18);
p2.setperson("李四",19);
p1.display();
p2.display();
return 0;
}
由上述代码可以看出,如果想设定某人的姓名及年龄可以通过setperson函数来实现,但是每次创建对象都要调用该方法设置信息,较为麻烦,所以就想到可不可以在创建对象时,将信息直接设置?由此就引出了构造函数。
构造函数是一个特殊的成员函数,名字与类名相同,当创建类对象时,编译器会自动调用,保证每个数据成员都有一个合适的初始值,并且在此对象的生命周期内只调用一次。
3.2.2 构造函数的特性
- 函数名与类名相同。
- 无返回值。
- 类进行实例化对象时编译器自动调用对应的构造函数。
- 构造函数可以重载。
- 当我们不提供构造函数时,会进行空实现,即编译器会自动调用一个无参的默认构造函数,当定义了构造函数后,编译器将不会自动生成。
需要注意的是:构造函数主要的任务并不是开辟内存空间创建对象,而是初始化对象。
还有一点需要注意的是:无参构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。无参构造函数、全缺省构造函数、没写编译器默认生成的构造函数,都可以认为是默认成员函数。
#include<iostream>
using namespace std;
#include<string.h>
class Person
{
public:
Person()
{
cout << "调用无参构造函数" << endl;
}
Person(string name,int age)
{
_name = name;
_age = age;
cout << "调用有参构造函数" << endl;
}
private:
string _name;
int _age;
};
int main()
{
Person p1;//调用无参构造函数
Person p2("张三",18);//调用有参构造函数
return 0 ;
}
运行结果如下图所示:
3.2.3 构造函数的调用规则
- 如果用户定义有参构造函数,则c++不再提供默认无参构造函数,但是会提供默认拷贝构造。
- 如果用户定义拷贝构造函数,c++不会再提供其他的构造函数。
3.3 析构函数
3.3.1 析构函数的概念
析构函数和构造函数的功能正好相反,析构函数是对象在销毁时会自动调用析构函数,完成类的资源清理工作。
3.3.2 析构函数的特性
- 析构函数名是在类名前加上字符~。
class Person
{
~Person()//析构函数
{
}
};
- 无参数无返回值。
- 一个类有且只有一个析构函数,如果用户没有定义,则系统会自动生成默认的构造函数。
- 类实例化的对象生命周期结束时,c++编译器会自动调用析构函数。
#include<iostream>
using namespace std;
#include<string.h>
class Person
{
public:
Person(string name,int age)
{
_name = name;
_age = age;
cout << "调用有参构造函数" << endl;
}
~Person()
{
cout << "调用析构函数" << endl;
}
private:
string _name;
int _age;
};
int main()
{
Person p1("张三",18);//调用有参构造函数
return 0 ;
}
此代码运行结果为:
由上图运行结果可得,当此类对象结束时,自动调用析构函数。
3.4 拷贝构造
3.4.1 拷贝构造的概念
在创建对象时,有时需要创建两个一模一样的对象,这是就要用到拷贝构造。
拷贝构造函数:只有单个形参,该形参是对本类类型形参的引用(一般常用const修饰),用在已存在的类类型对象创建新对象时由编译器自动调用。
3.4.2 拷贝构造的特征
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个并且必须使用引用传参,如果使用传值方式会引发无穷递归调用。
- 若用户没有自定义拷贝构造函数,系统会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝
这里需要注意的是,当类内定义的变量不是指针变量时,浅拷贝就可以完成拷贝构造,但是如果类内定义了指针变量时,浅拷贝会出现报错,此时需要使用深拷贝。
首先举一个浅拷贝的例子。
#include<iostream>
using namespace std;
#include<string.h>
class Person
{
public:
Person(string name,string sex,int age)
{
_name = name;
_sex = sex;
_age = age;
}
Person(const Person& p)
{
_name = p._name;
_sex = p._sex;
_age = p._age;
}
string _name;
string _sex;
int _age;
};
int main()
{
Person p1("张三","男",18);
cout<< "p1的姓名:"<< p1._name<<'\t'<< "p1的性别:"<< p1._sex<<'\t'<< "p1的年龄:"<< p1._age<<endl;
Person p2(p1);
cout<< "p2的姓名:"<< p2._name<<'\t'<< "p2的性别:"<< p2._sex<<'\t'<< "p2的年龄:"<< p2._age<<endl;
return 0;
}
此程序的运行结果即为函数的浅拷贝:
3.4.3 浅拷贝和深拷贝的区别
浅拷贝:简单的复制拷贝操作
深拷贝:在堆区重新申请空间,进行拷贝操作
接下来简单说一下深拷贝,如下述代码
#include<iostream>
using namespace std;
#include<string.h>
class Person
{
public:
Person(string name,string sex,int age)
{
_name = name;
_sex = sex;
_name = age;
}
Person(const Person& p)
{
_name = p._name;
_sex = p._sex;
_age = p._age;
}
string _name;
string _sex;
int *_age;
};
int main()
{
Person p1("张三","男",18);
cout<< "p1的姓名:"<< p1._name<<'\t'<< "p1的性别:"<< p1._sex<<'\t'<< "p1的年龄:"<< p1._age<<endl;
Person p2(p1);
cout<< "p2的姓名:"<< p2._name<<'\t'<< "p2的性别:"<< p2._sex<<'\t'<< "p2的年龄:"<< p2._age<<endl;
return 0;
}
以浅拷贝的方式进行操作的话,此程序运行会崩溃。原因是在定义类成员变量的时候,定义了一个int*类型的成员变量,给p1赋值之后,p1中的年龄指向内存中的一块空间,即存放着_age的内存空间,如果使用浅拷贝的话,p2直接拷贝p1,会出现p2的年龄也指向同一块内存空间,此时p1和p2的_age指针指向同一块内存空间,当程序继续运行,执行析构函数时,先对p1中指针变量所指向的空间进行释放清理,当执行对p2中指针变量所指向的空间进行释放清理时,此内存空间已经被释放清理过一次了,不可以再次进行释放清理,所以程序会崩溃。解决方法就是采用深拷贝的方式完成拷贝构造。
#include<iostream>
using namespace std;
#include<string.h>
class Person
{
public:
Person(string name,string sex,int age)
{
_name = name;
_sex = sex;
_age = new int (age);
}
Person(const Person& p)
{
_name = p._name;
_sex = p._sex;
_age = new int (*p._age);
}
~Person()
{
if(_age != NULL)
{
delete _age;
}
}
string _name;
string _sex;
int *_age;
};
int main()
{
Person p1("张三","男",18);
cout<< "p1的姓名:"<< p1._name<<'\t'<< "p1的性别:"<< p1._sex<<'\t'<< "p1的年龄:"<< *p1._age<<endl;
Person p2(p1);
cout<< "p2的姓名:"<< p2._name<<'\t'<< "p2的性别:"<< p2._sex<<'\t'<< "p2的年龄:"<< *p2._age<<endl;
return 0;
}
如上述代码所示,即为深拷贝,在拷贝时,在堆区创建新的空间存放拷贝的数据,让指针指向新的内存空间,这样执行析构函数时就不会出现程序崩溃的情况。(具体后续再总结)
4. 赋值运算符重载
4.1 运算符重载
运算符重载是具有特殊函数名的函数。
函数名字为:关键字operator后边接需要重载的运算符符号。
函数原型:返回类型 operator操作符(参数列表)
需要注意的是:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载运算符必须有一个类类型或者枚举类型的操作数
- 用于内置类型的操作运算符,其含义不能改变,例如:内置的整型+,不能改变其含义。
- 作为类成员的重载函数时,其形参看起来比操作数数目少一个,其实是成员函数的操作符有一个默认的形参this,限定为第一个形参。
- .*(调用成员函数的指针) 、::(域作用限定符) 、sizeof(计算变量所占内存的大小) 、?:(三目运算符) 、.(结构体变量引用符) 以上五个运算符不能重载。
以下程序重载运算符==
#include<iostream>
using namespace std;
#include<string.h>
class Person
{
public:
Person(string name,string sex,int age)
{
_name = name;
_sex = sex;
_age = age;
}
bool operator==(const Person& p)
{
return _age == p._age;
}
string _name;
string _sex;
int _age;
};
int main()
{
Person p1("张三","男",19);
Person p2("李四","男",19);
if(p1 == p2)
{
cout << p1._name << "和" << p2._name << "年龄相同" <<endl;
}
else
{
cout << p1._name << "和" << p2._name << "年龄不相同" <<endl;
}
return 0;
}
此时运行结果为:
#include<iostream>
using namespace std;
#include<string.h>
class Person
{
public:
Person(string name,string sex,int age)
{
_name = name;
_sex = sex;
_age = age;
}
bool operator==(const Person& p)
{
return _age == p._age;
}
string _name;
string _sex;
int _age;
};
int main()
{
Person p1("张三","男",18);
Person p2("李四","男",19);
if(p1 == p2)
{
cout << p1._name << "和" << p2._name << "年龄相同" <<endl;
}
else
{
cout << p1._name << "和" << p2._name << "年龄不相同" <<endl;
}
return 0;
}
此时的运行结果为:
可见此时实现了==运算符的重载。
4.2 赋值运算符重载
#include<iostream>
using namespace std;
#include<string.h>
class Person
{
public:
Person()
{
}
Person(string name,string sex,int age)
{
_name = name ;
_sex = sex ;
_age = age ;
}
Person& operator=(const Person& p)
{
if(this != &p)
{
_name = p._name;
_sex = p._sex;
_age = p._age;
}
}
string _name;
string _sex;
int _age;
};
int main ()
{
Person p1("张三","男",18);
Person p2;
p2 = p1;
cout<< "p2的姓名:"<< p2._name<<'\t'<< "p2的性别:"<< p2._sex<<'\t'<< "p2的年龄:"<< p2._age<<endl;
return 0;
}
此时运行结果为:
可以看出完成了赋值运算符的功能,但是要区分开拷贝构造和赋值运算符。拷贝构造函数用于创建新对象并初始化它,而赋值运算符用于修改已存在对象的值。
赋值运算符需要注意四点:
- 参数类型
- 返回值
- 检测是否自己给自己赋值
- 返回*this
5. const成员
5.1 const修饰类的成员函数
先看下列代码:
#include<iostream>
using namespace std;
#include<string.h>
class Person
{
public:
Person(string name,string sex, int age)
{
_name = name;
_sex = sex;
_age = age;
}
void Show()
{
cout<<"姓名:"<<_name<<'\t'<<"性别:"<<_sex<<'\t'<<"年龄:"<<_age<<endl;
}
private:
string _name;
string _sex;
int _age;
};
void func(Person p)
{
p.Show();
}
int main()
{
Person p1("李四","男",19);
func(p1);
return 0;
}
在上述代码中,虽然可以正常运行,但是当定义了一个Person类的p1后,调用func函数,如果传值的话,需要先拷贝一份,再进行打印出来,会浪费不必要的空间,所以func函数传参时可以进行传引用,就不需要再拷贝一份了。当传引用时,如果所传的值不需要发生改变的话,一般来说在前面都要加上const,如下所示,为修改成传引用。
void func(const Person& p)
{
p.Show();
}
如果将代码改成传引用,其他地方不进行修改的情况下,会出现报错。我们先看一下报错的原因是什么
这个时候我们就要提到一个知识点。
c++允许权限的缩小,但是禁止权限放大,非const引用不能指向const对象,但是反过来const引用可以指向非const对象。
当编译器调用func函数时,会将p的地址传给p.Show();即实际上为p.Show(&d);,在前边我们说过,类成员函数中会有一个隐含的this指针,将d的地址传入,该指向d的this指针的类型为const Person*,而我们在定义类的时候,隐含的this指针的类型为Person*,
所以,此时const Person *引用Person *,属于权限放大,所以会出现报错。
此时应该在void Show()函数中也加入const修饰,但是现在又出现了一个问题,我们在之前学习的时候已经替代了,this指针是隐式的,不可以显式定义,所以此时就引出了这样一种定义。
void Show() const//在Show()后加const 就相当于 void Show(const Person* this)
{
cout<<"姓名:"<<_name<<'\t'<<"性别:"<<_sex<<'\t'<<"年龄:"<<_age<<endl;
}
即此时是这样的
可以看到运行结果正常输出
所以,在使用const时,一定要注意权限的缩小放大。
接下来还需要注意区分的几个内容:
- const Person* p1 ->const修饰*p1(指向的对象)
- Person const* p2 ->const修饰*p2(指向的对象)
- Person* const p3 ->const修饰p3(指针本身)
- 第一点和第二点没区别,一般来说都是写第一种形式。
同样的,对于成员函数之间也是这样的
func1()
{}
func2() const
{}
//此时只能func1()调用func2() 属于权限缩小
//func2()不可以调用func1() 属于权限放大
总结以上内容:
- const对象可以调用非const成员函数吗?
不可以 - 非const对象可以调用const成员函数吗?
可以 - const成员函数内可以调用其它的非const成员函数吗?
不可以 - 非const成员函数内可以调用其它的const成员函数吗?
可以
6. 成员初始化
我们在前边创建有参构造函数后,创建对象时的成员赋了一次值,但是这并不是初始化。
class Person
{
public:
Person(string name ,string sex, int age)
{
_name = name;
_sex = sex;
_age = age;
}
private:
string _name;
string _sex;
int _age;
};
int main()
{
Person p1("李四","男",19);//此时并非初始化
return 0;
}
构造函数体中的语句只能称为是赋初值。因为初始化只能初始化一次,而构造函数体内可以多次赋值。而且如果出现引用或者const赋初值就不成立,所以此时就需要用到初始化。
6.1 初始化列表
先说一下初始化列表的格式,以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。,代码如下所示
class Person
{
public:
Person(string name ,string sex, int age)
:_name(name)
,_sex(sex)
,_age(age)
{
}
private:
string _name;
string _sex;
int _age;
};
需要注意以下几点:
- 每个成员变量初始化只能初始化一次(可以多次赋值)
- 类中包含(①引用成员变量、②const成员变量和③自定义类型成员即没有默认构造函数)这三类必须要放在初始化列表位置进行初始化。
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a ;
};
class B
{
public:
B(int a,int ref)
:_aobj(a)
,_ref(ref)
,_n(10)
{}
private:
A _aobj; //没有默认构造函数
int& _ref; //引用
const int _n; //const
};
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
#include<iostream>
using namespace std;
class A
{
public:
A(int a1,int a2)
:_a1(a1)
,_a2(_a1)
{}
void Show()
{
cout << _a1 << '\t' << _a2 <<endl;
}
private:
int _a2;
int _a1;
};
int main()
{
A A1(10,20);
A1.Show();
return 0;
}
例如上述程序,在声明时,先声明的_a2后声明的_a1,但是在初始化的时候,列表顺序时_a1在前,_a2在后。我们看一下运行结果。
_a2的值为0,_a1的值为10,可以看出,初始化的顺序并非初始化列表的顺序,而是声明的顺序。
6.2 explicit关键字
#include<iostream>
using namespace std;
#include<string.h>
class Person
{
public:
Person(string name = "张三", string sex = "男" , int age = 18)
:_name(name)
,_sex(sex)
,_age(age)
{}
void Show()
{
cout << _name << '\t' << _sex << '\t' << _age <<endl;
}
private:
string _name;
string _sex;
int _age;
};
int main ()
{
Person p1("张三","男",18);
p1.Show();
p1 = {"李四","男",19};
p1.Show();
return 0;
}
如上所示代码中,可以对p1直接进行赋值。运行结果如下:
实际编译器会用{}内的值构造一个无名对象,最后用无名对象给p1对象进行赋值。如果在默认构造函数前加上explicit修饰,将会禁止对p1赋值这样的隐式转换。
6.3 成员赋值新方法
在目前c++中,支持非静态成员变量在声明时进行初始化赋值,但是要注意这里不是初始化,这里是给声明的成员变量缺省值。
#include<iostream>
using namespace std;
#include<string.h>
class Person
{
public:
Person(){}
void show()
{
cout << _name << '\t' << _sex << '\t' << _age <<endl;
}
private:
string _name = "张三";
string _sex = "男";
int _age =18 ;
};
int main()
{
Person p;
p.show();
return 0;
}
此程序运行结果为:
7. static成员
7.1 static成员的概念
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称为静态成员变量。
7.2 特点
- 静态成员为所有类对象共享,不属于某个具体的对象。
- 静态成员变量必须在类外定义,定义时不添加static关键字。
- 类静态成员即可用类名::静态成员或者对象.静态成员来访问
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员。
- 静态成员和类的普通成员一样,也有public、protected、private 3种访问级别,也可以具有返回值。
面试题:实现一个类,计算程序中创建出了几个类对象。
#include<iostream>
using namespace std;
class A
{
public:
A()
{
++_count;
}
A(const A& t)
{
++_count;
}
static int Getcount()
{
return _count;
}
private:
static int _count;
};
int A::_count = 0;
int main()
{
cout<<A::Getcount()<<endl;
A a1, a2;
cout<<A::Getcount()<<endl;
A a3(a1);
cout<<A::Getcount()<<endl;
return 0;
}
此程序的运行结果为:
问题
- 静态成员函数可以调用非静态成员函数吗?
不能//因为静态成员函数中没有this指针 - 非静态成员函数可以调用类的静态成员函数吗?
不能
8. 友元
友元分为:友元函数和友元类。
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
8.1 友元函数
下边举一个必须用到友元函数的例子。
我们在上边重载了很多运算符,接下来我们试一下重载左移运算符(即operator<<)。先按照重载其他运算符一样,试一下重载左移运算符。
#include<iostream>
using namespace std;
#include<string.h>
class Person
{
public:
Person(string name ,string sex, int age)
: _name(name)
, _sex(sex)
, _age(age)
{}
void operator<<(ostream & out)
{
out <<_name <<endl;
}
private:
string _name;
string _sex;
int _age;
};
int main()
{
Person p("张三","男",18);
p << cout;
return 0;
}
当程序是这样时,运行结果为:
可以看出,这时就把张三给输出了,但是一个问题是,我们使用的输入输出都是cout或者cin在前,想要输出的后,而在此程序中,反过来了,降低了代码的可读性。如果把参数的部分改成对象(this指针就没办法指向对象),又不能成功输出,所以此时,我们只能在类外实现。因为类外实现的话,我们要传进去两个参数,不像在类内实现,只需要传一个参数,另外一个用隐含的this指针代替。
#include<iostream>
using namespace std;
#include<string.h>
class Person
{
public:
Person(string name ,string sex, int age)
: _name(name)
, _sex(sex)
, _age(age)
{}
private:
string _name;
string _sex;
int _age;
};
void operator<<(ostream & out,const Person& p)
{
out <<_name <<endl;
}
int main()
{
Person p("张三","男",18);
cout << p;
return 0;
}
我们把此重载函数放在类外之后,会发现一个问题,在类外无法访问_name,所以,这个时候就要用到友元了。友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
#include<iostream>
using namespace std;
#include<string.h>
class Person
{
friend void operator<<(ostream & out,const Person& p);
public:
Person(string name ,string sex, int age)
: _name(name)
, _sex(sex)
, _age(age)
{}
private:
string _name;
string _sex;
int _age;
};
void operator<<(ostream & out,const Person& p)
{
out <<p._name <<endl;
}
int main()
{
Person p("张三","男",18);
cout << p;
return 0;
}
此时的运行结果为:
可以看到,此时就正常输出姓名了,但是还是有一个问题,这样的话,只能一个一个输出,不能像之前那样全部输出,这时候就需要在返回值处修改一下,将空返回改成引用返回,这样的话在输入一个<<即可接着输出。
#include<iostream>
using namespace std;
#include<string.h>
class Person
{
friend ostream& operator<<(ostream& out,const Person& p);
public:
Person(string name ,string sex, int age)
: _name(name)
, _sex(sex)
, _age(age)
{}
private:
string _name;
string _sex;
int _age;
};
ostream& operator<<(ostream& out,const Person& p)
{
out <<p._name << '\t' << p._sex << '\t' <<p._age;
return out;
}
int main()
{
Person p("张三","男",18);
cout << p <<endl;
return 0;
}
此时的运行结果为:
可见此时,就完成了正常的输出。cin也是同样的道理。
注意:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数。
- 友元函数不能用const修饰。
- 友元函数可以在类定义的任何地方声明,不受访问限定符的限制。
- 一个函数可以是多个类的友元函数。
- 友元函数的调用与普通函数的调用和原理相同。
8.2 友元类
友元类的所有成员函数可以是另外一个类的友元函数,都可以访问另一个类中的非公有成员。
需要注意两点:
- 友元关系时单向的,不具有双向性。
- 友元关系不能传递。
class Date; // 前置声明
class Time
{
friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有员变量,反过来,Time类不可以访问Date类中的私有成员
public:
Time(int hour, int minute, int second)
: _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;
};
9. 内部类
在一个类中定义另一个类,这个内部的类就叫做内部类。注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。而内部类就是外部类的友元类。注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特点:
- 内部类可以定义在外部类的public、protected、private都是可以的。
- 注意内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
- sizeof(外部类)=外部类,和内部类没有任何关系。
class A
{
private:
static int k;
int h;
public:
class B
{
public:
void foo(const A& a)
{
cout << k << endl;//OK
cout << a.h << endl;//OK
}
};
};
int A::k = 1;
int main()
{
A::B b;
b.foo(A());
return 0;
}