文章目录
第四章 基于对象的编程风格
面向对象编程概念:
面向对象编程概念的两项最主要特质是∶继承(inheritance)和多态(polymorphism)。前者使我们得以将一群相关的类组织起来,并让我们得以分享其间的共通数据和操作行为,后者让我们在这些类之上进行编程时,可以如同操控单一个体,而非相互独立的类,并赋予我们更多弹性来加入或移除任何特定类。
一般而言,class由两部分组成:一组公开的(public)操作函数和运算符以及一组私有的(private)实现细节。
- 这些操作函数和运算符称为class的成员函数(member function),代表这个class的公开接口,用户只能访问公开接口。
- class的private实现细节可有member function的定义以及与此class相关的任何数据组成。
4.1 如何实现一个class
class的声明以关键字class
开始,其后接一个class名称
class Stack;
class 定义由两部分组成:class的声明和紧接其后的主体。主体内的两个关键字public
和private
,用来标示每个块的member访问权限。
- public member可以在程序的任何地方被访问,private member只能在member function或是class friend内被访问:
class Stack{
public:
//任何操作函数如果执行成功,就返回true
//pop和peek(查看)会将字符串内容置于elem内
bool push( const string& );
bool pop( string &elem );
bool peek( string &elem );
bool empty();
bool full();
//size()定义于class本身中,
//其他member这里只是声明
int size() {return _stack.size(); }
private:
vector<string> _stack;
//习惯上在data member之前加下划线
};
所有member function都必须在class主体内进行声明。
如果要在class主体内定义,这个member function会自动被视为inline函数(比如上述的size()
)。
1)主体内定义:见上 size()
2)主体外定义
- 在主体之外定义member function,必须使用特殊的语法(类名+类作用域解析运算符class scope resolution)告诉编译器该函数究竟属于哪一个class。
//两个冒号即类作用域解析运算符
bool Stack::empty()
{
return _stack.empty();
}
4.2 构造函数和析构函数
- 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
- 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
1)构造函数
- 如果我们提供一个或多个特别的函数对data member进行初始化,编译器会在每次class object被定义出来时调用适当的函数加以处理。这些特别的初始化函数称为constructor(构造函数)。
1.函数构造法
-
第一种初始化语法 :constructor的函数名称必须与class名称相同,不指定返回类型,亦不用返回任何值,可以被重载**:
class Triangular { public: //一组重载的constructor Triangular(); //default constructors Triangular(int len){ // 类内定义构造函数 _length = len; _beg_pos = 1; _next = 0; }; Triangular(int len, int beg_pos): _length(len), _beg_pos(beg_pos) { cout << "构造函数初始化列表" << endl; }; void cout_elems()const { cout << "_length:" << _length << "\n_beg_pos:" << _beg_pos << endl; } private: int _next; int _beg_pos; int _length; }; //默认的构造函数的定义方式1 //类主体外定义构造函数 Triangular::Triangular() { _length=1; _beg_pos=1; _next=0; } int main() { Triangular t1; // 默认调用无参数构造函数 Triangular t2(5); Triangular t3(5, 3); t1.cout_elems(); t2.cout_elems(); t3.cout_elems(); return 0; }
Triangular t1;
会对t1
应用default constructor(无需任何参数的constructor);
Triangular t2(10, 3);
会调用带有两个参数的构造函数,括号内的值被视为传给constructor的参数;Triangular t3 = 8;
会调用带有单一参数的constructor,而不是赋值运算( ? 我推测是因为会重载运算符);
Triangular t5();
无法成功定义Triangular
对象,而是将t5
定义为一个函数,参数列表为空,返回Triangular
对象——因为C++兼容C,对C而言,t5
后带小括号会使其被视为函数。Triangular t5; // ok
2.成员初始化列表
-
成员初始化列表
是构造函数的第二种初始化语法,主要用于将参数传给member class object的constructor
,参数列表里保护传址符&
class Triangular { public: // ... private: int _next,_length,_beg_pos; } //紧接在参数列表最后的冒号后面,是个以逗号分隔的列表, Triangular::Triangular(const Triangular &rhs) // & 传址, : _length(rhs._length), // 欲赋值给member的数值被放在member名称后面的小括号中: _beg_pos(rhs._beg_pos), _next(rhs.beg_pos - 1) { } //对,这个大括号里是空的 //rhs是类对象,这里引用了类对象且不会对类对象做任何修改
2)析构函数
-
析构函数是用户自定义的一个class member。在其类对象结束生命周期时,由析构函数处理善后这个类对象。**析构函数主要用来释放在**构造函数或类对象生命周期中分配的资源(构造函数初始化了数据成员(给了数据成员初值和内存)即为分配给数据成员资源!!)。
-
析构函数的名称有严格规定:class名称前加上
~
前缀。绝对不会有返回值,也没有任何参数,其参数列表是空的,因而也不能被重载:class Matrix { public: Matrix(int row, int col) : _row(row), _col(col) { //构造函数进行资源的分配 _pmat = new double[row * col]; } ~Matrix() { //析构函数进行资源的释放 delete [] _pmat; } private: int _row, _col; double* _pmat; }; //通过Matrix本身的构造函数和析构函数,完成了内存的自动管理
1)如果本类中一个成员变量是别的对象的指针,而且这个指针不是传进来的地址而是这个指针指向的对象,是在本类中(如果是栈里的定位分配,也不用考虑内存)在堆中开辟的空间创建的。并且该指针没有进行过delete操作,那么久需要在析构方法中进行delete操作,此时我们就必须自己写析构函数 。
#include <iostream>
using namespace std;
//日期类
class Date {
int year, month, day;
public:
//构造函数
Date(int y, int m, int d): year(y), month(m), day(d) {
cout << "调用了日期类的构造方法" << endl;
}
//对象销毁时 如果我们自己没有写析构方法,编译器会帮我们写一个然后调用
~Date() {
cout << "日期对象销毁了 ~Date()" << endl;
}
};
//员工类
class Employee {
string name;
Date *birthday;//声明一个指针 在构造方法里面让它指向一个对象
public:
//构造方法
Employee (string name): name(name) {
birthday = new Date(1989, 8, 8); //这里会调用日期的构造方法
cout << "调用了员工类的构造方法" << endl;
}
//析构方法
~Employee() {
cout << "员工对象销毁了" << endl;
}
};
int main() {
Employee em("假如我是张三");
return 0;
}
从运行结果可以知道创建的日期对象并没有销毁,所以有内存泄漏!
为什么泄漏了?因为出了mian函数,员工对象销毁,两个成员变量name和日期指针birthday在员工销毁的时候弹栈了,但是birthday指针指向的堆内存并没有销毁。所以应该在指针销毁之前释放指针指向的内存空间。
那么应该在哪里释放?员工对象在销毁的时候一定调用析构函数,所以在析构方法里对指针birthday进行delete,先把堆里开辟的内存空间清除掉,然后这个员工对象再销毁,所有内存才没有问题。
重写员工的析构函数如下:
~Employee() {
delete birthday;//清除指针指向的内存空间
cout << "员工对象销毁了" << endl;
}
2)全局区的对象在程序一开始就创建,程序结束才会销毁。栈区的对象在调用函数(代码块,如for循环里面)的时候才会创建,出了函数就会销毁。 在堆中开辟空间创建的对象必须我们自己手动delete。
重写main函数如下: (仔细看打印)
int main() {
Employee em("假如我是张三");
cout << "===== 1 栈里的对象 ======" << endl;
for (int i = 0; i < 3; i++) {
Date d(2015, 1, 23);
}
cout << "===== 2 ======" << endl;
{
Date d2(2015, 1, 2);
Date d3(2015, 1, 3);
Date d4(2015, 1, 5);
cout << "===== 3 ====" << endl;
}
cout << "=== 4 ====" << endl;
{
//这个代码块里会调用几次构造方法??0次
//因为这里只是创建指针并没有创建开辟空间对象
Date *d5;
Date *d6;
Date *d7;
}
cout << "=== 5 ====" << endl;
return 0;
}
3)成员逐一初始化操作(复制对象)
Triangular tril1(8);
Triangular tril2 = tril2;
默认情形下,当我们以某个类对象作为另一个对象的初值时,class的成员对象会被依次复制。
注意:当有指针和地址类相关成员时,无法进行复制。如在Matrix类中,两个对象的_pmat
会指向同一个数组。当该数组空间被释放时,会出现严重的错误。
如何解决 ?可以通过copy constructor改变这种 成员逐一初始化 的行为模式
Matrix::Matrix(const Matrix &rhs)
: _row(rhs._row), _col(rhs._col)
{
//对rhs._pmat所指的数组产生一份完全复本
int elem_cnt = _row * _col;
_pmat = new double[elem_cnt];
for(int ix = 0;ix < elem_cnt;++ix)
_pmat[ix] = rhs._pmat[ix];
}
4)标注const表明class object的内容不可变
class Triangular {
public:
//以下是const member function
int length() const {return _length;}
int beg_pos() const {return _beg_pos;}
int elem(int pos) const;
//主体外定义时,
//必须同时在声明和定义中指定const
//以下是non-const member function
bool next(int &val);
void next_reset() { _next = _beg_pos - 1; }
//...
private:
int _length; //元素个数
int _beg_pos; //起始位置
int _next; //下一个迭代目标
static vector<int> _elems;
};
//主体外,定义也要包含const
int Triangular::elem(int pos) const
{
return _elems[pos - 1];
}
4.3 mutabl(e可变)和const(不变)
1)const(不变的数据成员)
(2)为什么会有在函数参表后加一个const呢?
为了确保类对象的值(内容)不会被更改,必须要确保成员函数都不会更改他们的调用者——类对象的值(内容),所以类设计者要在成员函数的参表后加一个const来告诉编译器:这个成员函数不会更改类对象的内容
(2)const修饰符紧接在函数参表之后,在class主体以外定义的成员函数,如果是一个const成员函数,那么就必须同时在声明与定义中指定const,编译器会检查每个声明为const的成员函数(函数参表后加了个const),检查他们是否真的更改了类对象的内容,如果更改了,编译器报错
- class主体内定义const函数
class Triangular{
public:
//以下是const成员函数
int length() const{return _length;}//最好)和const中间加一个空格,不加也行。
int beg_pos() const{return _beg_pos;}
int elem(int pos) const;
//以下时非const成员函数
bool next(int &val);
void next_reaset()
{
_next=_beg_pos-1;
}
static vector<int>_elems;//静态数据成员(以后说明)
- class主体外定义const函数。
int Triangular::elem(int pos)const//在这里指定const
{
return _elems[pos-1];
}
(3)注意,提防着标注着const的成员函数返回一个非const引用,引用(“指向”)标注着const的成员函数的函数体内的对象
虽然没有修改标注着const的成员函数的函数体内的对象,但是这样做实际上是将标注着const的成员函数的函数体内的对象开放了出去(因为可以通过非const的引用来修改类对象的内容),允许程序在其他地方加以修改,如:
/*这里,虽然我val()成员函数(参表为空,返回值是BigClass的非const引用)的参表后标着const,意思是val()这个成员函数不会更改类对象的内容,但是我返回的是一个非const的引用“指向”着_val。这不就是把_val开放了出去(因为可以通过非const的引用来修改_val,也就是类对象的内容),允许程序在其他地方可以修改_val,也就是类对象的内容了!
*/
class val_class{
public:
val_class(const BigClass &v)
:_val(v){}
//采用成员初始化列表的初始化语法(方式)
//来定义val_class类的构造函数
BigClass& val()const
{
return _val;
}
private:
BigClass _val;
};
-
如何解决呢?
我们知道成员函数可以根据其参表后加没加const而决定是否重载本身(成员函数)。所以我们提供const版本的和非const版本的成员函数的定义。如下:class val_class{ public: const BigClass& val()const{return _val;} BigClass& val(){return _val;} };
-
非const的类对象会调用非const的成员函数val(),const的类对象调用参表后标注着const的成员函数val()。如下:
void example(const BigClass *pbc,BigClass &rbc) { //为了区分const版本和非const版本,让这两个版本的比较直观一些 //所以参表这么设计, //注意果const位于*的左侧,则const就是用来修饰指针所指向的变量, //即指针指向为常量(该指针为函数指针,指向的函数参表是空的 //且返回类型也会带个const什么什么。(毕竟函数指针的数据类型 //要和它指向的函数的返回类型是一致的!)) //第二个参数是一个引用,引用非const的成员函数 pbc->val();//调用const版本的成员函数val() rbc.val();//调用非const版本的成员函数val() }
2)mutable(可变的数据成员)
(1)关键字mutable
可以让我们做出这样的声明:对数据成员所做的改变并不会破坏类对象的常量性。先看下面这个例子:
class Triangular{
public:
bool next(int &val)const;
void next_reset()const
{
_next=beg_pos-1;
}
//...
private:
int _next;
int _beg_pos;
int _length;
};
int sum(const Triangular &trian)
{
if(!trian.length())
{
return 0;
}//类对象的长度这个数据成员是0的话,退出sum函数体
int val,sum=0;
trian.next_reset();
while(trian.next(val))
{
sum+=val;
}
return sum;
}
- 注意_next这个数据成员(私有成员)。trian是个const类对象,而next_reset()函数和next()函数都会更改_next的值,这两个函数都不是const成员函数,但却被trian这个类对象调用。这是个语法错误
- 所以一开始我们会想到必须把next_reset()函数和next()函数改为const成员函数,但是这两个函数的函数体内确实改变了私有成员数据成员_next的值。
(2) 使用关键字mutable,
class Triangular{
public:
bool next(int &val)const;
void next_reset()const
{
_next=beg_pos-1;
}
//...
private:
mutable int _next;
int _beg_pos;
int _length;
};
现在next_reset()函数和next()函数既可以修改_next的值,又可以被声明为const成员函数了,这里面关键字mutable起的作用就是告知编译器数据成员(这里为私有成员)_next的值可变,就是让这个数据成员变得“特殊”,改变“mutable 数据类型 对象名”的数据成员(这里为私有成员)不会破坏类对象的常量性!
4.4 this指针
-
this指针,是一个能在成员函数里指向调用者(即类对象)的指针。我们需要一种可以指向整个类对象的指针,就是this指针。
-
欲以一个对象复制出另一个对象,先确定两个对象是否相同是一个好习惯(
if(this != &rhs)
),这必然运用this指针。Triangular& Triangular::copy(const Triangular &rhs) { //检查两个类对象是不是私有成员公共接口什么的相同不相同, //不相同复制个屁。 if(this!=&rhs) { _length=rhs._length; _beg_pos=rhs._beg_pos; _next=_rhs._beg_pos-1; } return *this;//不管能不能复制,都返回this指针指向的调用者(用copy成员函数的类对象) } tr1.copy(tr2);//返回的由this指针所指的类对象, //这句话就把tr2这个类对象作为了tr1这个类对象的初值
①我们可以在定义类成员函数的时候,用this指针去访问this指针指向的类对象的私有成员,利用这个特点从而去进行别的操作。
②我们可以在定义类成员函数的时候提领this指针来解决return谁的问题
return *this;
代码实践:
#include <iostream>
#include <string>
using namespace std;
class Teacher {
public:
Teacher() {}
Teacher(int age, string name, int tel_nember): _age(age), _name(name), _tel_number(tel_nember) {}
Teacher ©(const Teacher &rhs) {
if (this != &rhs) {
_age = rhs._age;
_name = rhs._name;
_tel_number = rhs._tel_number;
}
}
void display_name() {
cout << this->_name << endl;
}
private:
int _age;
string _name;
int _tel_number;
};
int main() {
Teacher A(18, "A", 1234);
A.display_name();
A.copy(A); //调用的对象与自己相同
Teacher B;
B.copy(A); //实际是这样 copy(&B, A);
B.display_name();
return 0;
}
4.5 静态类成员
数据成员可以分静态变量、非静态变量两种
静态成员:静态类中的成员加入static修饰符,即是静态成员.可以直接使用类名+静态成员名访问此静态成员,因为静态成员存在于内存,非静态成员需要实例化才会分配内存,所以静态成员不能访问非静态的成员…因为静态成员存在于内存,所以非静态成员可以直接访问类中静态的成员.静态成员在每个类中只有一个拷贝,是解决同一个类的不同对象之间数据和函数共享问题的。
非成静态员:所有没有加Static的成员都是非静态成员,当类被实例化之后,可以通过实例化的类名进行访问…非静态成员的生存期决定于该类的生存期…而静态成员则不存在生存期的概念,因为静态成员始终驻留在内容中。
一个类中也可以包含静态成员和非静态成员,类中也包括静态构造函数和非静态构造函数。
1)静态成员变量和静态成员函数
- 类中,static(静态) data member用来表示唯一的、可共享的member
- 静态成员变量可以在类的多个对象中访问,但是要在类外声明。不同对象访问的其实是同一个实体,它可以在同一类的所有对象中被访问。
- 普通成员和对象是绑定的,随对象的创建和释放而生死(不管在栈里还是堆里),类似于局部变量和
malloc
堆内存。静态成员变量在对象中不占用存储空间,其实是放在全局变量空间里的。 - 静态成员函数在类外实现时只需在类内声明时加
static
,类外实体无须加static
关键字,否则是错误的(因为在类外实体前面加static
会按照static
修饰全局函数类理解)。
#include<iostream>
#include<string>
using namespace std;
class person
{
public:
int age; //普通成员变量
static int nums; //静态成员变量
static void print(void); // 静态成员函数
};
int person::nums = 1;
//static void person::print(void) 错误写法
void person::print(void) //正确写法
{
cout << "print()." << endl;
}
int main(){
person p1, p2;
cout << p1.nums << " " << p2.nums << endl;
p1.nums = 10;
cout << p1.nums << " " << p2.nums << endl;
person::print();
system("PAUSE");
return 0;
}
- 静态成员变量和方法也可以根本不产生对象而用类本身来调用,语法是
类名::静态成员变量
。
int person::nums = 1;
int main(){
cout << person::nums << endl;
person::nums = 10;
cout << person::nums << endl;
system("PAUSE");
return 0;
}
- 静态成员函数在类外实现时只需在类内声明时加
static
,类外实体无须加static
关键字,否则是错误的(因为在类外实体前面加static
会按照static
修饰全局函数类理解)
2) 静态成员和普通成员的互相调用规则
- 普通成员函数中可以调用静态成员变量和方法,调用方法有3种:
- 1.直接访问
- 2.
this
指针访问- 3.
类名::func()
方式访问
class person
{
public:
int age; //普通成员变量
static int nums; //静态成员变量
static void print(void);
void func(void)
{
//print(); 方法1
//this->print();方法2
person::print();//方法3
}
};
void person::print(void)
{
cout<<"static function print()."<<endl;
}
- 静态方法中只能访问静态成员变量和方法,不能访问任何非静态的东西,否则会编译报错。静态方法中如果确实需要访问非静态成员,应该通过函数传参方式。
class person
{
public:
int age; //普通成员变量
static int nums; //静态成员变量
static void print(void);
};
void person::print(void)
{
this->age=10; // 报错
}
3)静态成员和非静态成员的区别
- 访问方式不一样.静态成员(通过类名.静态成员名访问).非静态成员(通过对象名.非静态成员名访问)
- 静态成员属于类.该类的所有对象共同拥有这一个成员。非静态成员属于对象,每个对象都有一份.静态成员不论有类有多少个对象.只在内存中分配一块空间.
4)代码实践
#include <iostream>
#include <vector>
#include <functional>
#include <algorithm>
using namespace std;
class Myclass {
public:
Myclass() {
m = 10;
};
static int getn(Myclass a);// 静态成员函数
static int z; // 公共静态数据成员
private:
int m; // 非静态数据成员
static int n; // 静态数据成员
};
int Myclass::n = 100; // 静态数据成员初始化
int Myclass::z = 50; // 公共静态数据成员初始化
int Myclass::getn(Myclass a) {
// cout << m << endl; //对x的引用是错误的
cout << a.m << endl; // 通过类间接使用 非静态数据成员,正确的
cout << n << endl; // 正确的
cout << a.n << endl; // 正确的
return n; // 直接使用 静态数据成员
}
int main() {
Myclass app1;
cout << app1.getn(app1) << endl; // 利用对象引用静态函数成员
cout << Myclass::getn(app1) << endl; // 利用类名引用静态函数成员
cout << Myclass::z << endl;
}
4.6 打造一个Iterator Class
1) 运算符重载
运算符函数很像普通函数,但是运算符函数不用指定函数名。只需要在运算符前加上关键字operator即
运算符重载的规则如下:
- 不可以引入新的运算符。(除了
.
、.*
、::
、?
其他运算符皆可被重载; - 运算符的操作数个数不可
- 运算符的优先级不可改变;
- 运算符函数的参数列表中,必须至少有一个参数为class类型。(无法为non-class类型,重新定义其原已存在的运算符或引进新运算符)。
- 非成员函数形式定义的运算符的参表中,会比成员函数形式定义的运算符多出一个参数,即this指针(该this指针自动代表左操作数)
2)嵌套类型
typedef
为某个类型设定另一个不同的名称:typedef existing_type new_name;
,其中的existing_type
可以是任何内置类型、复合类型或class类型。
typedef 内置类型/复合类型/class类型 new_name;
//new_name是该类型的别名
4.7 友元函数,友元类
同类无私处,异类有友元
在 C++中,一个类中可以有 public、protected、private 三种属性的成员,通过对象可以访问 public 成员,只有本类中的函数可以访问本类的 private 成员。借助友元(friend),可以使得其他类中的成员函数以及全局范围内的函数访问当前类的 private 成员。
1)友元函数
声明函数时,在前面加friend
关键字,这样就构成了友元函数。友元函数可以是不属于任何类的非成员函数,也可以是其他类的成员函数。
friend 返回类型 函数名(参数表);
① 将非成员函数声明为友元函数。
#include <iostream>
using namespace std;
class Student {
public:
Student(char *name, int age, float score);
friend void show(Student *p); //将show声明为友元函数
private:
char *m_name;
int m_age;
float m_score;
};
// 构造函数:成员逐一初始化操作
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score) {}
// 非成员函数
void show(Student *p) {
//调用了private 成员,原则上不能通过对象访问,友元函数可以访问。将第7行注释掉,观察编译器的报错信息。
cout << p->m_name << "的年龄是:" << p->m_age << ",成绩是:" << p->m_score << endl;
}
int main() {
Student stu("小明", 15, 90.6);
show(&stu); //调用友元函数
Student *pstu = new Student("李磊", 16, 80.5);
show(pstu); //调用友元函数
return 0;
}
注意,友元函数不同于类的成员函数,在友元函数中不能直接访问类的成员,必须要借助对象。下面的写法是错误的:
void show(){
cout<<m_name<<"的年龄是 "<<m_age<<",成绩是 "<<m_score<<endl;
}
成员函数在调用时会隐式地增加 this 指针,指向调用它的对象,从而使用该对象的成员;而 show() 是非成员函数,没有 this 指针,编译器不知道使用哪个对象的成员,要想明确这一点,就必须通过参数传递对象(可以直接传递对象,也可以传递对象指针或对象引用),并在访问成员时指明对象。
② 将其他类的成员函数声明为友元函数
#include <iostream>
using namespace std;
class Address; //提前声明Address类
//声明Student类中的成员
class Student {
public:
Student(char *name, int age, float score);
void show(Address *addr); // Student类中的成员函数
private:
char *m_name;
int m_age;
float m_score;
};
//声明Address类中的成员
class Address {
private:
char *m_province; //省份
char *m_city; //城市
char *m_district; //区(市区)
public:
Address(char *province, char *city, char *district);
//将 Student 类的成员函数 show() 声明为 Address 类的友元函数,show() 就可以访问 Address 类的 private 成员变量了
friend void Student::show(Address *addr);
};
//实现Student类
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score) { }
void Student::show(Address *addr) {
// Student类的成员函数可以直接防御类的成员变量
cout << m_name << "的年龄是 " << m_age << ",成绩是 " << m_score << endl;
// 声明了友元函数,可以通过对象调用类的成员变量
cout << "家庭住址:" << addr->m_province << "省" << addr->m_city << "市" << addr->m_district << "区" << endl;
}
//实现Address类
Address::Address(char *province, char *city, char *district) {
m_province = province;
m_city = city;
m_district = district;
}
int main() {
//第一种类的new和调用
Student stu("小明", 16, 95.5f);
Address addr("陕西", "西安", "雁塔");
stu.show(&addr);
//第二种类的new和调用
Student *pstu = new Student("李磊", 16, 80.5);
Address *paddr = new Address("河北", "衡水", "桃城");
pstu -> show(paddr);
return 0;
}
注意: 一个函数可以被多个类声明为友元函数,这样就可以访问多个类中的 private 成员。
2)友元类
类与类之间也可以建立friend关系,比如class A与class B去建立关系,这样A类里的所有成员函数都成为了B类的friend,同理B类里的所有成员函数都成为A类的friend。
例如将类 B 声明为类 A 的友元类,那么类 B 中的所有成员函数都是类 A 的友元函数,可以访问类 A 的所有成员,包括 public、protected、private 属性的。
class A {
public:
friend class B;
private:
...
}
更改上例的代码,将 Student 类声明为 Address 类的友元类:
#include <iostream>
using namespace std;
class Address; //提前声明Address类
//声明Student类
class Student {
public:
Student(char *name, int age, float score);
void show(Address *addr);
private:
char *m_name;
int m_age;
float m_score;
};
//声明Address类
class Address {
public:
Address(char *province, char *city, char *district);
//将Student类声明为Address类的友元类
friend class Student;
private:
char *m_province; //省份
char *m_city; //城市
char *m_district; //区(市区)
};
//实现Student类
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score) { }
void Student::show(Address *addr) {
cout << m_name << "的年龄是 " << m_age << ",成绩是 " << m_score << endl;
cout << "家庭住址:" << addr->m_province << "省" << addr->m_city << "市" << addr->m_district << "区" << endl;
}
//实现Address类
Address::Address(char *province, char *city, char *district) {
m_province = province;
m_city = city;
m_district = district;
}
int main() {
Student stu("小明", 16, 95.5f);
Address addr("陕西", "西安", "雁塔");
stu.show(&addr);
Student *pstu = new Student("李磊", 16, 80.5);
Address *paddr = new Address("河北", "衡水", "桃城");
pstu -> show(paddr);
return 0;
}
- 任何class都可以将其他function或class指定为friend,使其具备与class member function相同的访问权限,可以访问class的private member。
- 只要在某个函数的原型前加上关键字
friend
,就可以将它声明为某个class的friend。这份声明可以出现在class定义的任意位置(不受private或public的影响)。 - 如果希望将数个重载函数都声明为某个class的friend,必须明确的为每个函数都加上关键字friend。
4.8 拷贝构造函数和拷贝赋值运算符
对于一个类,里面包含了一个指针成员(私有成员),然而这个类我们声明两个对象,然后一个对象给另一个对象赋值,采用默认的成员逐一复制操作的话(即前者=后者;
)就会让两个类的指针成员都指向一个堆里的动态数组,要是这两个类对象的析构函数都作用的话,前者析构对象作用后,delete掉了这个堆里的动态数组,那么,后者的指针成员就成了个野指针,显然这种方法就不对。
所以需要一个拷贝构造函数和拷贝赋值运算符,重新定义=
Matrix& Matirx::
operator=(const Matrix &rhs)//很像重载运算符一样
{
if(this!=&rhs)//这里this指针指代"="左边的类对象。
{//要是类对象间赋值(复制=右边的类对象所有public和private成员给
//=左边的类对象),如果俩类对象都是一样的,那你=还有什么意义?
//浪费时间(直接跳过if)。
_row=rhs._row;
_col=rhs._col;
int elem_cnt=_row*_col;
delete []_pmat;
_pmat=new double[elem_cnt];
//至于delete到new这两行,我的理解就是解除因=左边这个类对象
//的构造函数让其指针成员指向了=右边的类对象里的new出来的数组
//的这个指向。然后我让其指针成员指向自己new出来的数组,然后
//给这个数组的所有元素赋值=右边的类对象new出来的数组的所有元素就可以了。
//卧槽虽然看起来不像这个意思但是作者是这么想的。
for(int ix=0;ix<elem_cnt;++ix)
{
_pmat[ix]=rhs._pmat[ix];
}
}
}
4.9 实现一个 function operator
- 所谓函数对象就是一种提供有函数调用运算符的类
//LessThan.h文件
#ifndef _LESSTHAN_H_
#define _LESSTHAN_H_
#include <iostream>
#include <vector>
using namespace std;
class LessThan {
public:
LessThan(int val): _val(val) {}; //采用成员初始化列表语法形式的LEssThan类构造函数
int comp_val()const {
return _val;//因为这个类的每个类对象在定义的时候都必须提供一个参照数(你跟谁比,比它小还是大的怎么怎么着)(comp_val),因此这是参照数的读取
}
void comp_val(int nval) {
_val = nval; //参照数的写入,这是一个重载函数(comp_val重载函数)
}
bool operator()(int value)const;//运算符函数的声明,供LessThan(参数)这样的()运算的表达式使用。定义放在程序代码文件里。
private:
int _val;//供public定义的函数用。
};
int count_less_than(const vector<int> &vec, int comp);
void print_less_than(const vector<int> &vec, int comp, ostream &os = cout);
//在LessThan.h所附属的.cpp文件里的两个非成员函数的声明在此
#endif // _LESSTHAN_H_
//LessThan.cpp文件
#include "LessThan.h"
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
inline bool LessThan::
operator()(int value)const { //最好函数声明和定义的参数表的参数名一致。
return value < _val; //小于return true,否则return false
}
//函数调用运算符的实现。然而重载运算符的运算符函数的定义必须和用到的重载运算符的
//地方或函数要放在一个文件里,否则编译器报错
int count_less_than(const vector<int> &vec, int comp) {
//从vector容器里挑出比某个参照数小的元素,计数有多少个这样的元素
LessThan lt(comp);//定义LessThan类对象。(就像LessThan lt10(10);)
//(和定义一般对象一样的定义方式,只是这个类对象需要有一个参照数(初始值,即类对象定义的时候就被初始化了)
int count = 0;
for (int ix = 0; ix < vec.size(); ix++) {
if (lt( vec[ix] )) //()运算符重载(针对了lt这个类对象在()前的形式的重载),所以这个就是个判断vector容器里的某个元素是否比comp这个参照数小,
//这里提示了undefined reference to `LessThan::operator()(int) const'|||error: ld returned 1 exit status|
//小的话if(true),否则if(false)
{
++count;//比参照数小,计入count里
}//否则什么也不做
}
return count;//返回有多少个比参照数小的元素个数
}
void print_less_than(const vector<int> &vec, int comp,
ostream &os) //②默认值**只能指定一次**,在函数声明或定义,不能全给指定了,这是
//默认参数值的提供的两个规则之一,另一个是①默认值的解析操作从最右边开始进行,
//也就是说我们给某个参数提供了默认值则其右边的所有参数必须拥有默认值!(该规则主要体现在函数声明或定义中)
//否则编译器报错
{
LessThan lt(comp);//定义LessThan类对象。(就像LessThan lt10(10);)
//(和定义一般对象一样的定义方式,只是这个类对象需要有一个参照数(初始值,即类对象定义的时候就被初始化了)
vector<int>::const_iterator iter = vec.begin(),
it_end = vec.end(); //常量型泛型指针的定义(常量型泛型指针不会改变指向的容器的内容,
//同时这么写会告诉程序读者我不会改变泛型指针指向的容器的内容。
os << "elemts less than " << lt.comp_val() << endl; //调用类对象里的public(公共)成员函数
while ((iter = find_if(iter, it_end,
lt)) != it_end) //找出第一个比lt类对象里的参照数小的元素(关于find_if()这个泛型算法的函数原型请查阅资料)
//那个()注意不要括错了,会有
//error: no match for 'operator=' (operand types are 'std::vector<int>::const_iterator {aka __gnu_cxx::__normal_iterator<const int*, std::vector<int> >}' and 'bool')|
//这样的报错
//资料:(注意,用了泛型算法,得包含#include <algorithm>!!!!!!!!!!
/** template<class InputIterator, class Predicate>
InputIterator find_if ( InputIterator first, InputIterator last, Predicate pred )
{
for ( ; first!=last ; first++ ) if ( pred(*first) ) break;
return first;
}
*/
//它在区间[first,end)中搜寻使一元判断式pred为true的第一个元素。如果没找到,返回end。
//通常我们把函数对象当作参数传给泛型算法,就体现在了find_if()这一行。
{
os << *iter << ' '; //输出小于参照数的vector容器的元素。
++iter;//泛型指针迭代,辅助输出符合条件的元素
}
}
//main.cpp文件
#include <iostream>
#include <vector>
#include "LessThan.h"
#include <algorithm>
using namespace std;
int main() { //测试。
int ia[16] = {17, 12, 44, 9, 18, 45, 6, 14, 23, 67, 9, 0, 27, 55, 8, 16};
vector<int>vec(ia, ia + 16);
//vector容器的建立和初始化
int comp_val = 20; //参照数的设置
cout << "Numeber of elements less than " << comp_val << " are " << count_less_than(vec, comp_val) << endl;
//小于参照数的vector容器里的元素有多少个
print_less_than(vec, comp_val); //输出上述的这些个元素
return 0;
}
4.10 重载iostream运算符
为什么有要重载iostream运算符?
因为我们想对某个类对象进行读写操作。不重载iostream类运算符直接cout<<类对象<<endl;
或cin>>类对象;
编译器会报错
1)重载ostream运算符
直接cout<<类对象<<endl;
编译器会报错,所以我们要有个重载<<运算符函数,如:
ostream& operator<<(ostream &os,const Triangular &rhs)
{
os<<"("<<rhs.beg_pos()<<","
<<rhs.legnth()<<")";
rhs.display(rhs.length(),rhs.beg_pos(),os);
return os;
//传入重载运算符函数的ostream对象又被原封不动地返回
}
int main(){
Triangular tri(6,3);
cout<<tri<<endl; // << 已经被重载了
}
- ostream对象没有被声明为const,是因为每一次输出操作都会更改ostream对象的内部状态(查资料可详细了解如何更改ostream对象的内部状态。
- 使用&,是基于效率考虑而非为了修改其对象内容。
2)重载istream运算符
istream& operator>>(istream &is,Triangular &rhs)
{
char ch1,ch2;//定义两个字符用来规定
//>>运算符后的输入格式,像scanf函数里可规定输入格式那样
int bp,len;//起始位置和数列长度的定义
is>>ch1>>bp>>ch2>>len;
//假设输入为(3,6) 6 10 15 21 28 36
//那么ch1=='(',bp==3,ch2==',',len==6。
rhs.beg)pos(bp);
rhs.length(len);
rhs.next_reset();
//输入的数据对应传给传进来的类对象(rhs)的数据成员。
return is;
//传入重载运算符函数的istream对象又被原封不动地返回
}
4.11 指向类成员函数的指针
1) 指向类成员的指针
- C++扩展了指针在类中的使用,使其可以指向类成员,这种行为是类层面的,而不是对象层面的。
- 指向类成员/函数的指针的本质并不是取地址.而是利用了对象地址的偏移量
- C++提供了指向类成员的指针: 变量类型
类名::*pointer = &类名::变量名
如:string Student::*pstr1 = &Student::name;
#include <iostream>
#include <string>
using namespace std;
class Student {
public:
Student(string n, int num): name{n}, num{num} {} // 构造函数:成员初始化列表
void display(int idx) {
cout << idx << " " << name << " " << num << endl;
}
public:
string name;
int num;
};
int main() {
Student s1("majin", 100);
Student *ps1 = &s1;
Student s2("moqun", 80);
Student *ps2 = &s2;
string Student::*pstr1 = &Student::name;
cout << s1.*pstr1 << endl; //不同的对象可以调用同一个指针
cout << ps1->*pstr1 << endl;
cout << s2.*pstr1 << endl;
cout << ps2->*pstr1 << endl;
return 0;
}
2)指向类成员函数的指针
- 指向类成员函数的指针,总归是函数指针,有返回类型,有参表。只不过这个指针指向的函数是某个类里的成员函数
- 返回值类型
(类名::*ptr)(函数参数) = &类名:: 成员函数
如void (Student::*pdis)(int) = &Student::dis;
- 调用方法与成员对象指针类似: 因为优先级问题要加上括号
下面看成员函数指针的案例:
// 这个案例中,我们不想让外界知道类内部的函数名,我们可以是用指向成员函数的指针数组将它们封装起来,加强了隐蔽性
#include <iostream>
using namespace std;
class Game
{
public:
Game(){
PSkill[0] = &Game::SkillOne;
PSkill[1] = &Game::SkillTwo;
PSkill[2] = &Game::SkillThree;
PSkill[3] = &Game::SkillFour;
}
void select(int index) {
if (index >= 0 && index <= 3) {
(this->*PSkill[index])();
}
}
private:
void SkillOne(){ cout << "Use skill one.." << endl; }
void SkillTwo(){ cout << "Use skill Two.." << endl; }
void SkillThree(){ cout << "Use skill Three.." << endl; }
void SkillFour(){ cout << "Use skill Four.." << endl; }
enum {
NC = 4 //技能数量
};
void (Game::*PSkill[NC])(); //函数数组
};
int main(){
Game newOne;
newOne.select(2);
newOne.select(0);
newOne.select(3);
system("PAUSE");
return 0;
}
小结:
public公共接口
- 对象生命或定义
- 成员函数声明或定义
private私有成员
- 对象声明或定义
- 成员函数声明或定义
实现一个类,分文件式
- 类主体class{}放在头文件里
- 类成员函数的部分或全部定义放在头文件附属的程序代码文件里
- 主函数程序代码文件,可以调用类的公共接口(public)里的成员函数,但
私有成员只能成员函数调用或者友元类里访问
析构函数和构造函数
带有构造函数和析构函数的类对象被定义后,编译器自动调用构造函数为其初始化,程序执行到其生命周期结束编译器自动调用析构函数释放其所占的内存
构造函数:用来初始化类里的私有成员
- 放在public里:
- 可以重载构造函数
- 成员初始化列表法定义类的构造函数
- 普通法定义类的构造函数(类主体外带::的像定义普通函数一样的定义方式)
- 创建类对象时后面附着()参表,就相当于定义类对象同时初始化类对象里的私有成员
- 需要配合
析构函数
,因为构造函数和析构函数定义内部产生出来的对象都在堆内存里。
析构函数:在类对象的生命周期结束后析构函数进行其资源(内存)的释放,需要手动定义
- 其定义可以放在类主体的公共接口里和构造函数定义一起出现
~构造函数名()
没有参数,没有返回值。
mutable(可变)和const(不变)
mutable
- 用于修改一些辅助的变量如泛型指针(迭代器)但这不破坏类对象的内容。所以const缀在成员函数参表后,成员函数内部修改的不破坏类对象内容的私有成员在private:上写成mutable 类型名 对象名;
const
- 可以用于成员函数定义或声明上,在函数参表后缀一个const,代表这个成员函数不会更改类对象的内容
this指针
- 直接访问类私有成员的途径之一
- 在成员函数定义(内部)出现,指向调用这个成员函数的类对象
静态类成员
- 在一个
class
内用static
修饰成员变量,则被修饰的成员变量即为静态成员变量,用static
修饰成员方法,则被修饰的成员方法即为静态成员方法。 - 所有类对象共享这一个静态类成员,增删查改都方便(类比qq群文件资源)
友元函数,友元类
- 友元是一种定义在类外部的普通函数,但它需要在类体内进行声明,为了与该类的成员函数加以区别,在声明时前面加以关键字friend。友元不是成员函数,但是它可以访问类中的私有成员。友元的作用在于提高程序的运行效率,但是,它破坏了类的封装性和隐藏性,使得非成员函数可以访问类的私有成员。
- 在某类里设定其他类为friend,那就是在某类的主体内写friend class 其他类的类名;这样其他类的成员函数都是某类的friend,都可以访问某类的私有成员
- 当一个类作为另一个类的友元时,这就意味着这个类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的非公有成员。