文章目录
小点
- 数据成员类内初始值只能放在等号
=
右边, 或者放在花括号{ }
里, 不能使用圆括号()
。 - 成员函数的定义虽然处于类定义的花括号之外, 但还是在类作用域内,所以可以自由访问类的成员, 不需要成员访问语法。
成员函数在类内和类外定义略有差别: 在类定义的花括号内定义的成员函数默认为inline 函数。如果要在类外定义inline 成员函数, 需要显式地在函数声明或定义前加关键字inline - 在组织类代码时, 一个类的定义通常分为两个文件:== 类及其成员的声明放在头文件中==,成员函数的类外定义放在源文件中, 并在其中包含头文件。需要注意的是,类成员的声明不能分割在不同的文件中。类定义的头文件和类实现的源文件一般使用相同的名字,在使用类的客户程序中只需要包含头文件即可。
-使用包含守卫的头文件格式如下:
#ifndef HEADER_H
#define HEADER_H
//Class
#include<string.h>
#endif
- 访问限定符
struct A
{
public:
//公有成员声明
private:
//私有成员声明
protected:
//被保护成员声明
};
没有指定则默认为public
-
public 成员在程序的任何函数或类中都可以被访问。public 用于说明类接口中的成员,客户程序通过public 成员可以操纵该类型的对象。
private 成员只能由类自己的成员函数或友元访问, 需要隐藏的信息应该声明为private
protected 成员的访问权限介于public 和private 之间, 主要用于继承中。protected 成员可以由类自己的成员函数、友元、派生类成员访问。 -
C++中引入
class
,与struct不同,class
默认访问限定为private
-
每个对象都有自己的数据成员, 类的成员函数定义并不在各个对象中存储, 而是整个类存储一份, 本类的所有对象共享这些成员函数的定义。因此, 简单对象在内存中占据的存储空间是所有数据成员大小的和。如果类中包含复杂的成员, 情况可能并非如此。或者编译器实现为了访问效率而采用边界对齐技术的话, 对象的大小将是机器字长的整数倍。
sizeof
运算符可以用于类类型、对象和类的数据成员。 -
每个成员函数都有一个隐含的参数, 指向接收消息的对象, 称为this 指针。X 类的this指针的类型是X*。this 指针是一个常量, 不能改变this 指针的值, 也不能取this 指针的地址。
-
this 在成员函数中最常用于:
- 区分与局部变量重名的数据成员;
- 返回当前对象;
- 获取当前对象的地址。
如下:
class X
{
int m;
public:
void set(int m)
{
this->m=m; //区分与函数参数重名的数据成员
}
X& add(const X& a)
{
m+=a.m;
return *this; //返回当前对象
}
void copy(const X& a) //复制对象
{
if(this==&a) //判断是否为同一对象
return;
m=a.m;
}
};
-
编译器一般用对象在内存中的地址作为对象的唯一标识。因此, 判断两个对象是否相同时不是比较它们的属性值是否相等, 而应该比较它们的内存地址是否相等。
-
将数据成员限定为private , 并提供public成员函数来对其进行访问, 这种成员函数被称为访问器(accessor) 和修改器(mutator)。数据成员xx 的访问器函数一般命名为getxx , 修改器函数名为setxx。有时候只需要一个。
//height的访问器和修改器
public:
double getHeight()
{
return height;
}
void setHeight(double newheight)
{
if(newheight>0)
{
height=newheight;
}
}
friend
(友元)函数让一个非成员函数可以访问一个类的私有数据。在声明友元时要遵循一条规则: 友元必须在被访问的类中声明。一个类的友元可以是全局函数、另一个类的成员函数或另一个类。类A 是类B 的友元隐含着A 的所有成员函数都是B 的友元。
class X;
class Y
{
public:
void f(X*);
};
class X
{
int i;
public:
void initialize();
friend void g(X*,int);
friend void Y::f(X*);
friend class Z;
friend void h();
};
void X::initialize()
{
i=0;
}
void g(X* x,int i)
{
x->i=47;
}
void Y::f(X* x)
{
x->i=47;
}
class Z
{
int j;
public:
void initialize();
void g(X* x);
};
void Z::initialize()
{
j=9;
}
void Z::g(X* x)
{
x->i+=j;
}
void h()
{
X x;
x.i=100; //h为X友元,正确
}
int main()
{
X x; Z z;
Z.g(&x); x.i=100; //错误 main()不是友元
}
虽然在某些场合,按照正确的方式使用友元有助于控制复杂度, 例如在某些设计模式中, 但一般情况下友元会破坏类的封装性, 增加类之间的耦合度, 因此,应该尽量避免使用友元。
进一步隐藏
访问控制允许将类的实现与类的接口分开, 使得客户程序不能轻易地访问私有实现部分。但是实现部分的隐藏并不彻底, 这可能导致以下问题
(1) 在安全性要求极高的领域,即使核心实现己经封闭在库中不可见,但是头文件中的成员声明仍然可能暴露一些内部信息,如果遇到恶意访问,会存在安全隐患。
//模拟银行账户
//以下是用户可以看到的头文件内容
//account.h
class Account
{
public:
void open(string no,string name,double account);//开户
void close(); //销户
double getBalance(); // 查询余额
void withdraw(double amount); // 取款
void deposit(double amount); // 存款
private:
string accNo; // 账号
string clientName; // 姓名
double balance = 0; // 余额
};
/*经过编译,用户不能看到类的实现的源码,
但不能防止客户程序恶意访问私有数据成员并篡改*/
#include "account.h"
int main()
{
Account acc;
//正常操作
acc.open("123456","Harry",200); //开户并存入200
cout<<"Account Balance : " <<acc.getBalance()<<endl;
acc.deposit(1000); //存钱
acc.withdraw(800); //取钱
//通过指针恶意操作
//取得acc对象首地址
unsigned char *address=(unsigned char*)(&acc);
//看了头文件知道有哪些数据成员,又知道对象的特点
// 算出余额成员balance的首地址
address=address+sizeof(acc)-sizeof(double);
double *key=(double*)address;
*key=9E10;
cout<<"Account Balance:"<<acc.getBalance()<<endl;
acc.withdraw(20000000); //偷钱成功
}
(2) 在设计初期, 实现部分经常需要变动, 就连头文件中类的私有成员声明也不时需要修改。这意味着无论何时修改了一个类, 无论修改的是公共接口还是私有成员的
声明部分, 都将导致重新编译包含了该头文件的所有文件, 增加不必要的编译时间。
解决这些问题的一种常用技术称为“ 句柄类( handle class ) ” 。可以将有关实现的所有内容进一步隐藏起来, 包括私有数据成员的声明,== 类定义中只留下公共接口声明和一个指向结构体的私有指针成员==, 而结构体的定义与所有成员函数的定义一同放置在实现文件中。
这样, 一方面进一步隐藏了内部信息, 有效地防止了外部程序通过指针或类型转换来设法访问类中的私有成员: 另一方面, 只要接口部分保持不变, 头文件就不必改动, 实现部分则可以按需要任意修改, 完成后只要对该类的实现文件重新编译即可。
//handle.h
//头文件中只包含公共接口和一个指针
//用户可以看到的内容
class Handle
{
//类的接口声明
public:
void initialize();
void cleanup();
int read();
void change(int);
private:
struct Inner; // 内嵌结构体的声明
Inner* pointer;
//数据成员都封装在一个Inner对象中,这里只有一个指向Inner的指针
};
//结构体的定义与所有函数的定义包含于实现文件 handle.cpp 中
//如果改变了数据结构Inner,头文件并不受影响,只有handle.cpp重新编译
//handle.cpp
#include"handle.h"
//Inner是一个嵌套在Handle类作用域中的结构体,因此使用类名限定
struct Handle::Inner()
{
int i;
};
void Handle::initialize()
{
pointer=new Inner; //为保存数据成员的结构体对象分配空间
pointer->i=0;
}
void Handle::cleanuup()
{
delete pointer; //释放存储空间
}
int Handle::read()
{
return pointer->i;
}
void Handle::change(int x)
{
pointer->i=x;
}
//useHandle.cpp
#include"handle.h"
int main()
{
Handle h;
h.initialize();
h.read();
h.change(10);
h.cleanup();
}
对上述Account类进行进一步封装:
//account.h
struct Data; //此处使用非嵌入的结构体
class Account
{
public:
void open(string no,string name,double amount);
double getBanlance();
void withdraw(double amount);
void deposit(double amount);
void close();
private:
Data *pa=nullptr;
}
//account.cpp
#include"account.h"
struct Data
{
string accNo;
string clientName;
double balance=0;
};
void Account::open(string no,string name,double amount)
{
if(pa!=nullptr)
return;
pa=new Data;
pa->accNo=no;
pa->clientName=name;
pa->balance=amount;
}
double Account::getBalance()
{
if(pa!=nullptr)
return pa->balance;
else
return 0;
}
void Account::withdraw(double amount)
{
if(pa!=nullptr)
if(amount>0&&amount<=pa->balance)
pa->balance-=amount;
}
void Account::deposit(double amount)
{
if(pa!=nullptr)
{
if (amount>0)
{
pa->balance+=amount;
}
}
}
void Account::close()
{
if(pa!=nullptr)
{
withdraw(getBalance());
delete pa;
}
}
构造函数和析构函数
构造函数
构造函数(constructor)是一种特殊的成员函数, 能够在创建对象时被自动调用, 负责对象的初始化。构造函数的名字和类名字相同, 它没有返回类型( 注意: 不是void 类型) 。构造函数的参数通常为数据成员提供初始值。构造函数可以重载, 在创建对象时, 编译器会根据初始值的类型和个数来调用相应的构造函数, 因而构造函数的形式决定了初始化对象的方式。
类中的有些成员不能使用赋值的方式提供初始值, 例如:
class X
{
int m;
int& r;//引用成员
public:
X(int v=0)
{
m=v; //正确
r=m; //错误:引用必须在定义时初始化
}
};
成员r 是引用类型, 不能用赋值的方式提供初值。对于const 数据成员和类类型的数据
成员存在类似问题。那么如何初始化这样的成员?
初始化由构造函数完成,引用成员的初始化也应该在构造函数中, 但是又不能在函数
体中使用赋值方式提供初值。针对这种情况有一种特殊的语法, 称为构造函数初始化列表。
初始化列表的形式如下:
成员1(初始值1) [ , 成员2(初始值2) , ... ]
初始化列表位于构造函数之后,函数体之前:
构造函数(参数表) : 初始化列表 { 函数体 }
例如:
class X
{
int m;
int& r;//引用成员
public:
X(int v=0):r(m)//初始化r,m则默认初始化,未定义
{
m=v; //给m赋值
}
};
普通成员也可以用这种格式进行初始化,如:
class X
{
int m;
int& r;
public:
X(int v=0):m(v),r(m)
{ }
};
这两种提供初值的方法是有差别的: 写在构造函数的函数体中, 是成员先
默认初始化, 再在此处赋值; 写在初始化列表中, 是直接初始化数据成员。显然, 使用初始化列表的效率更高。另外, 如果成员是const 、引用, 或者是未提供默认构造函数的类类型, 就必须通过构造函数初始化列表为这些成员提供初值。因此, 建议使用构造函数初始化列表语法。
在初始化列表中, 每个成员只能出现一次。成员初始化的顺序与它们在类定义中出现的顺序一致, 构造函数初始化列表中初始值的先后关系不会影响实际的初始化顺序。尽量避免用某些成员初始化其他成员。
委托构造函数
委托构造函数(delegating constructor):使一个构造函数可以调用另一个构造函数
委托构造函数有一个成员初始化列表和一个函数体。成员初始化列表只有唯一一项,即类名本身。类名后面紧跟参数列表, 参数列表必须与类中另一个构造函数匹配。
例如:
#include<iostream>
using namespace std;
class X
{
public:
X(int aa,int bb,int cc):a(aa),b(bb),c(cc) //构造函数1
{ cout<<"X(int,int,int)"<<endl; }
X(int aa,int bb):X(aa,bb,0) //2:委托1执行初始化
{ cout<<"X(int,int)"<<endl; }
X(int aa):X(aa,0,0) //3:委托1执行初始化
{ cout<<"X(int)"<<endl; }
X():X(1,1) //4: 委托2执行初始化,2又转而委托1
{ c=1; cout<<"X()"<<endl; }
private:
int a,b,c;
};
int main()
{
cout<<"1: "<<endl;
X one(1,2,3);
cout<<"2: "<<endl;
X two(1,2);
cout<<"3: "<<endl;
X three(1);
cout<<"4: "<<endl;
X four;
cin.get();
}
/*
Output:
1:
X(int,int,int)
2:
X(int,int,int)
X(int,int)
3:
X(int,int,int)
X(int)
4:
X(int,int,int)
X(int,int)
X()
*/
当一个构造函数委托另一个构造函数时, 受委托的构造数的初始化列表和函数体依次执行, 然后将控制权交还给委托者的函数体。
析构函数
析构函数( destructor ) 负责在对象生存期结束时返回相关资源和自动释放资源。当对象离开作用域时, 或者用delete 释放在堆一上创建的对象时, 析构函数都会被自动调用。
析构函数没有返回类型, 也没有任何参数。析构函数不能重载, 只能为一个类定义唯一一个析构函数。
一般情况下, 如果一个类只包含按值存储的数据成员, 则析构函数不是必须定义的。析构函数主要被用来放弃在类对象的构造函数或生存期中获得的资源, 如释放互斥锁或归还new 分配的空间。不过, 析构函数的作用并不局限在释放资源上, 一般地, 析构函数可以执行类设计者希望在最后一次使用对象之后执行的任何操作。如果类中没有定义析构函数, 编泽器在需要时会自动合成一个析构函数。
析构函数在大多数情况下都是被自动地隐式调用, 一般不要显式调用析构函数。
const成员函数
class X
{
int m;
public:
X(int v=0):m(v) {}
void set(int v) {m=v;}
int get() {return m;}
};
int main()
{
const X a;
a.set(10); //报错
a.get(); //报错
}
在这段代码中, b 不能进行set(), 只能调用get() 。但是编译器对这两条语句都会报告错误一一因为编译器不能从这两个函数的声明形式上区分哪个会改变对象, 哪个不会改变对象。
将一个成员数声明为const , 表明这个成员函数不会修改对象的数据成员, 能保证对象的常量性。
声明const 成员函数的语法形式为:
返回类型 成员函数名(参数表) const;
例如:
class X
{
int m;
public:
X(int v=0):m(v) {}
void set(int v) {m=v;}
int get() const {return m;}
};
int main()
{
const X a;
a.set(10); //报错
a.get(); //正确
}
只有声明为const 的成员函数才可以被const 对象调用。const 对象不能调用非const 成员函数, 但是非对象可以调用const 成员数。
const 成员承数中不能修改类的数据成员, 也不能调用其他非const 成员函数, 否则会引起编译错误。
X 类的成员函数的第一个隐含参数是X* 类型的this 指针。const 限定的成员函数其实是将const 关腱字作用于隐含的this 指针, 其类型成为了const X* 。因此, 编译器防止以任何方式通过this 指针来修改当前对象的状态, 从而保证了对象在其生存期间的常量性。
允许为一个成员函数定义const 和非const 两个版本, 这两个版本是重载函数。对const 对象, 会选择调用const 版本的成员函数; 对非const 对象, 则调用非const成员函数。
mutable成员
逻辑常量性:逻辑上看具有常量性,但仍需要改变某些成员的值
例如下代码:
class Date
{
public:
Date(int y,int m,int d);
string string_rep()const; //返回日期的字符串表示
private:
int year,month,day;
};
void func()
{
Date teachersday(2010,9,10);
cout<<teachersday.string_rep();
}
每次从Date 的成员构造这样一个字符串比较费时, 所以可以考虑保留字符串的一个副本, 在重复需要时直接返回这个副本即可, 即缓存技术。
为了实现缓存技术而设置的数据成员对用户而言是不可见的, 用户也无法看到在string_rep()
中所进行的修改和计算。因而string_rep()
函数实际上会修改相关成员的值, 所以它具有逻辑常量性。
声明如下:
class Date
{
public:
Date(int y,int m,int d);
string string_rep()const; //返回日期的字符串表示
private:
int year,month,day;
bool cache_valid; //缓存中的字符串是否有效
string cache; //保存字符串表示的缓存
void compute_cache_value(); //计算日期的字符串表示,填充缓存
};
//实现string_rep()方式一:采用const_cast进行强制转换
//这种实现方式既不美观,也不安全。
string Date::string_rep()const
{
if(cache_valid=false)//强制去掉当前对象的常量性,改变对象
{
Date* th=const_cast<Date*>(this);
th->compute_cache_value(); //调用非const成员函数
th->cache_valid=true; //修改数据成员
}
return cache;
}
//避免强制类型转换的方法是将涉及缓存管理的数据声明为mutable(易变的)
//const限定对mutable成员没有影响,mutable 成员在任何时候都是可以改变的。
void func()
{
Date teachersday(2010,9,10);
cout<<teachersday.string_rep();
}
(补充) const_cast使用:
const_cast
是一个基于C语言编程开发的运算方法,其主要作用是:修改类型的const
或volatile
属性。使用该运算方法可以返回一个指向非常量的指针(或引用)指向b1,就可以通过该指针(或引用)对它的数据成员任意改变。
const_cast<type_id> (expression)
为了允许在任何情况下都能够修改一个类的数据成员, 可以将该数据成员声明为mutable
mutable
数据成员永远不会是常量, 即使它是一个const
对象的数据成员。
class Date
{
public:
Date(int y,int m,int d);
string string_rep()const; //返回日期的字符串表示
private:
int year,month,day;
mutable bool cache_valid; //缓存中的字符串是否有效
mutable string cache; //保存字符串表示的缓存
void compute_cache_value()const; //计算日期的字符串表示,填充缓存
};
//实现string_rep()方式二:采用mutable
string Date::string_rep()const
{
if(cache_valid=false)
{
compute_cache_value();
cache_valid=true; //修改mutable数据成员,正确
}
return cache;
}
void func()
{
Date teachersday(2010,9,10);
cout<<teachersday.string_rep();
}
static成员
有时一个类的所有对象都需要访问某个共享的数据。例如, 一个带有对象计数器的类,这个计数器对当前程序中一共有多少个此类型的对象进行计数。使用全局变量安全性不能保证。
类的静态数据成员为上述问题提供了一种史好的解决方案。静态数据成员被当作类类型内部的全局变量。
静态数据成员在整个类中只有一份, 由这个类的所有对象共享访问。与全局变量相比, 静态数据成员有以下两个优点。
( 1 ) 静态数据成员没有进入程序的全局作用域, 只是在类作用域中, 因而不会与全局域中的名字产生冲突。
( 2 ) 可以实现信息隐藏, 静态成员可以是private 成员, 而全局变量不能。
class Object
{
static int count; //静态数据成员
public:
Object() { count++; } //其他所有重载构造函数都要有count++
~Object() { count--; }
int getCount() const { return count; }
};
static 数据成员不是属于某个特定对象的, 因而不能在构造函数中初始化。
static 数据成员在类定义之外初始化。在定义时要使用类名字限定静态成员名, 但不需要重复出现static 关键字。
下面是count 的初始化:
int Object::count=0;
static 成员只能定义一次, 所以定义一般不放在头文件中, 而是放在包含成员函数定义的源文件中。
静态数据成员与非静态数据成员之间主要有以下区别。
( 1 ) 从逻辑角度来讲,静态数据成员从属于类, 非静态数据成员从属于对象。
( 2 ) 从物理角度来讲,静态数据成员存放于静态存储区, 由本类的所有对象共享,生命期不依赖于对象。而非静态数据成员独立存放于各个对象当中, 生命期依赖于对象, 随对象的创建而存在, 随对象的销毁而消亡。静态数据成员可以是任何类型, 甚至是所属类类型。
静态成员只有一个副本,因此可以直接用类名字限定的静态成员名字访问。也可以用.
或->
访问
class object
{
static int count;
friend void func(Object& obj);
};
void func(Object& obj)
{
cout<<obj.count;
cout<<Object::count;
}
静态成员仍然遵循访问控制的约束。所以, 上面的func()被声明为Object 的友元才可访问其私有静态成员count
static成员函数
普通成员函数必须通过对象或对象的地址调用, 而静态数据成员并不依赖对象存在。如果成员函数只访问静态数据成员, 那么用哪个对象来调用这个成员函数都没有关系。这样的成员函数可以声明为静态成员函数。
class Object
{
static int count;
public:
Object() { count++; }
Object(const Object&) { count++; ... }
~Object() { count--; }
static int getCount() { return count; } //静态成员函数
};
静态成员函数可以直接用类名限定静态成员函数名调用。
静态成员函数没有this指针,因此静态成员函数不能访问非静态数据成员,也不能调用非静态成员函数。静态成员函数不能声明为const
和volatile
,因为二者是限定this指针的。
只包含静态成员函数和静态常量数据成员的类经常被称为工具类( utility class ) , 是面向对象类库中提供一组库函数的常用方式。
单件模式
设计模式的 一种,保证一个类仅有一个实例
#include<iostream>
using namespace std;
class Singleton
{
private: //构造函数是私有的,防止在外部创建对象
int num;
static Singleton* sp;
Singleton(int _num)
{
num=_num;
}
Singleton(const Singleton&)
{
//防止复制对象
}
public:
static Singleton* getInstance(int _num);
void handle()
{
if(num>0)
{
num-=1;
}
else
{
cout<<"num is zero!"<<endl;
}
}
};
Singleton* Singleton::sp=0;
Singleton* Singleton::getInstance(int _num)
{
if(sp==0) //检测是否只有一个实例
{
sp=new Singleton(_num);
}
return sp;
}
int main()
{
Singleton* sp=Singleton::getInstance(1);
sp->handle();
Singleton* st=Singleton::getInstance(10);
//调用getInstance并不会得到新对象
st->handle(); //num is zero!
cin.get();
}
指向成员的指针
如果有一个指向类成员的指针p , 要取得指针指向的内容, 必须用“ * ” 运算符解引用。但是 * p 是一个类的成员, 不能直接使用, 必须指定对象或指向对象的指针来选择这个成员,
所以使用指向成员的指针的语法应该如下:
对象.*指向成员的指针
对象指针->*指向成员的指针
定义指向成员的指针的定义语法如下:
成员类型 类名::*指向成员的指针
例如: int X::*p;
定义了一个指向X类中int类型的成员的指针
成员函数的指针
语法如下:
返回类型 (类名::*指向成员函数的指针) (参数表)
可以用一个成员函数的地址初始化成员函数指针,也可以在其他地方给指针赋值。
例如:
class Simple
{
public:
int f(float) const { return 1; }
};
int (Simple::*fp)(float) const;
int (Simple::*fp2)(float) const=&Simple::f;
int main()
{
fp=&Simple::f;
}
隐式类型转换构造函数
class X
{
int m;
public:
X(int v):m(v) { } //转换构造函数,可以将int型转换为X类型
};
void f(X obj) { }
int main()
{
int iv=10;
f(iv); //正确,调用X(int)进行隐含的参数类型转换
X obj1(iv); //正确,直接初始化,调用X(int)
X obj2=iv; //正确,拷贝初始化,调用X(int)
}
如果不希望编译器自动进行类型转换,可以在析构函数的声明前加上explicit
关键字禁止隐式转换
class X
{
int m;
public:
explicit X(int v):m(v) { }
};
explicit 构造数只能用于直接初始化。在执行拷贝形式的初始化时( 用= ) 也会发生隐式类型转换, 所以, 只能用直接初始化的形式使用explicit 构造函数。而且, 编译器将不会在自动转换过程中使用explicit 构造函数。例如:
int main()
{
int iv=10;
f(iv); //错误,不能将iv从int转换为X类型
X obj1(iv); //正确,直接初始化,调用X(int)
X obj2=iv; //错误,需要从int道X的转换
}
拷贝控制成员
当定义一个类时, 我们会显式或隐式地指定在此类型的对象复制、移动、赋值和销毁时做什么。一个类通常定义五种特殊的成员函数来控制这些操作, 包括:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数。拷贝和移动构造函数定义了当用同类型的另一个对象初始化当前对象时做什么。拷贝和移动赋值运算符定义了将一个对象赋值给同类型的另一个对象时做什么。析构函数定义了当此类型对象销毁时做什么。
这些操作被称为 拷贝控制(copy control) 操作。
如果一个类没有定义所有这些拷贝控制成员, 编译器会在需要时合成相应的操作
但是对有些类来说, 编译器合成的默认行为并不适合。因此, 需要理解什么时候需要定义这些操作。
拷贝构造函数
为了避免编译器合成的浅复制行为, 可以自己定义拷贝构造函数, 对包括指针和引用在内的成员进行恰当的初始化。
class X
{
int m;
int &r;
int* p;
public:
X(int mm=0):m(mm),r(m),p(&m) {}
X(const X&a):m(a.m),r(m),p(&m) {} //拷贝构造函数
void changep() { *p=10; }
void changer() { r=5; }
};
int main()
{
X a; //X(int): a.m=0; a.r=a.m; a.p=&a.m
X b(a); //X(const X&): b.m=0; b.r=b.m; b.p=&b.m
b.changep();//b.m=10
b.changer();//b.m=5,不影响a
}
拷贝构造函数的一般形式是X(X&)
或者X(const X&)
, 这两种形式是有差别的。
如果拷贝构造函数是X(X&)
, 那么就不能用const 对象来初始化另一个X类型的对象, 因为参数类型不匹配。
如果是X(const X&)
, 那么既可以用const 对象也可以用非const 对象来初始化另一个X类型的对象。
拷贝构造函数的形式为什么不是X(X)
呢?
如果用X 类的对象a 初始化X 类的对象b , 会引起拷贝构造数的调用。在调用X(Xobj)
时, 按值传参数是用实参a 初始化形参obj, 这同样是用一个对象初始化另一个同类对象,又需要调用拷贝构造函数, 重复这一过程, 显然会陷入对X(X)
的无限循环调用。因此拷贝构造函数的形参一定是引用。
以下三种情况都是用一个对象初始化另一个同类对象的语义, 会调用拷贝构造函数:
(1) 用一个对象显式或隐式初始化另一个同类对象;
(2) 函数调用时, 按传值方式传递对象参数;
(3) 函数返回时, 按传值方式返回对象。
class Y
{
public:
Y(int v=0):m(v) {}
Y(const Y& obj) { m=obj.m; } //拷贝构造函数
private:
int m;
};
void f(Y obj) { ... }
Y g()
{
Y obj;
...
return obj;
}
int main()
{
Y a;
Y c=a; //拷贝初始化,等同于Y c(a); 拷贝构造函数调用
Y b(a); //拷贝构造函数调用
f(a); //参数按值传递,构造函数调用
b=g(); //函数按值返回,构造函数调用
}
拷贝赋值运算符
类可以控制其对象如何赋值, 方法是重载赋值运算符。重载运算符本质上是函数,名字由关键字operator 后接要定义的运算符, 如名为operator=
的函数就是赋值运算符。
重载运算符之后, 类类型的对象就可以像内置类型一样使用运算符进行操作。
赋值运算符operator=()
是最常用的运算符之一, 也是定义类时经常要重载的一个运算符。
对于复杂的类,尤其是包含指针成员时,应显式地创建operator=()
赋值和初始化不同。初始化是在创建新对象时进行, 只能有一次, 而赋值可以对己存在的左值多次使用。类类型的对象在初始化时调用构造函数, 而赋值时调用operator=()
赋值运算符“ = ” 可能用在初始化对象的地方, 但是这种情况并不会引起operator=()的调用。赋值运算符的左操作数是已经存在的对象时, 才会调用operator=()
类X的赋值运算符要定义为类X的成员函数,形式为:
X& operator=(const X&)
为了与内置类型的赋值保持一致,赋值运算符通常返回一个指向左操作数的引用。
class Value
{
int a,b;
float c;
public:
Value(int aa=0,int bb=0,float cc=0.0)
: a(aa),b(bb),c(cc) { }
Value& operator=(const Value & rv)
{
a=rv.a;
b=rv.b;
c=rv.c;
return *this;
}
};
int main()
{
Value a, b(1, 2, 3.3); // a为(0,0,0)
a=b; // 调用operator= a变为(1, 2, 3.3)
}
上面的代码隐含了一个常见的错误:在对象赋值之前应该进行自赋值检测,即检验对象是否在给自身赋值。
自赋值有时会导致严重后果,如下:
class my_string
{
char* str;
int len;
public:
my_string(const char* s="")
{
len=strlen(s);
str=new char[len+1];
strcpy(str,s);
}
~my_string() { delete[]str; }
my_string& operator=(const my_string& s);
};
my_string& my_string::operator=(const my_string& s)
{
len=s.len;
str=s.str;
return *this;
}
my_string a("abcde"),b("hijk");
a=b;
这段代码中的直接赋值会导致两个问题:
第一, 赋值后a 和b 的str 指向了同一段存储空间, 破坏了对象的完整性, 如果对a 或b 中的一个字符串操作, 另一个也会受到影响。
第二, 原来的“ a.str 指向的动态存储空间没有释放, 会造成内存泄漏。
所以避免这种问题应该用深复制,修改代码如下:
my_string& my_string::operator=(const my_string& s)
{
delete[] str; //先释放当前对象中的动态存储空间
len=s.len;
str=new char[len+1]; //再重新分配空间
strcpy(str,s.str); //最后进行复制
return *this;
}
此时如果出现a=a
的语句,先释放左操作数a的str 指针,但右操作数(也是a)的指针同时也被释放了
造成这种后果的原因是没有在operator=() 中进行自赋值检测。
再次修改代码如下:
my_string& my_string::operator=(const my_string& s)
{
//赋值前先进行自赋值检测
if(this==&s)
return *this;
//深复制
delete[] str;
len=s.len;
str=new char[len+1];
strcpy(str,s.str);
return *this;
}
编写拷贝赋值运算符时, 要切记两点:
(1) 如果一个对象给自身赋值,赋值运算符必须能正确工作。
(2) 大多数赋值运算符组合了析构函数和拷贝构造函数的工作。
移动构造函数和移动赋值运算符
一般形式为X(X&&)
移动操作通常不分配资源, 因而不会抛出任何异常。标准库类型如vector 为了避免元素在移动构造时会出现问题, 在不能确定移动构造函数不会产生异常时, 会对元素使用拷贝构造函数而不是移动构造函数。因此, 不抛出异常的移动构造函数和移动赋值运算符必
须要标记为noexcept
, 语法形式如下:
//在类定义头文件的声明和定义中都要指定noexcept
X(X&&) noexcept;
X::X(X&&)noexcept : /*成员初始化列表*/
{
/*...*/
}
如果一个类既有移动构造函数, 又有拷贝构造函数, 编译器使用普通的函数匹配规则来确定使用哪个构造函数: 移动右值,复制左值。赋值操作的情况类似。
如果一个类有拷贝构造函数, 但是没有定义移动构造函数, 编译器通过拷贝构造函数来移动对象, 即用拷贝构造丞数代替移动构造函数。赋值运算符的情况类似。
只有一个类没有定义任何自己版本的拷贝控制成员, 并且类的每个非static 数据成员都可以移动时, 编译器才会自动合成移动构造函数和移动赋值运算符。
下面的代码定义了所有的控制成员:
#include<iostream>
#include<cassert>
using namespace std;
class IntArray
{
public:
IntArray(); //默认构造函数
IntArray(const IntArray& ia); //拷贝构造函数
IntArray(IntArray&& ia)noexcept; //移动构造函数
IntArray(int ia[],size_t size); //用内置数组ia初始化对象
explicit IntArray(int size); //指定数组大小的初始化
~IntArray(); //析构函数
IntArray& operator=(const IntArray& right); //拷贝赋值运算符
IntArray& operator=(IntArray&& right)noexcept; //移动赋值运算符
int& get(size_t index); //取数组元素,返回左值
const int& get(size_t index)const; //const版本
size_t size()const; //返回数组大小
private:
size_t arrSize=0; //数组大小
int *ptr=nullptr; //数组首地址
};
IntArray::IntArray():arrSize(0),ptr(nullptr) { }
IntArray::IntArray(const IntArray& ia):arrSize(ia.arrSize) //深复制初始化
{
ptr=new int[arrSize];
for(size_t id=0;id<arrSize;id++)
{
ptr[id]=ia.ptr[id];
}
}
IntArray::IntArray(IntArray&& ia) noexcept//移动初始化
:arrSize(ia.arrSize),ptr(ia.ptr)
{
ia.arrSize=0;
ia.ptr=nullptr; //被移走数据的对象置为空数组
}
IntArray::IntArray(int ia[],size_t size)
{
assert(size>0);
arrSize=size;
ptr=new int[arrSize];
for(size_t id=0;id<arrSize;id++)
ptr[id]=ia[id];
}
IntArray::IntArray(int size)
{
assert(size>0);
arrSize=size;
ptr=new int[arrSize];
for(size_t id=0;id<arrSize;id++)
ptr[id]=0;
}
IntArray::~IntArray()
{
delete[] ptr;
}
IntArray& IntArray::operator=(const IntArray& right) //拷贝赋值
{
if(this==&right)
return *this;
arrSize=right.arrSize;
delete[] ptr;
ptr=new int[arrSize];
for(size_t id=0;id<arrSize;id++)
ptr[id]=right.ptr[id];
return *this;
}
IntArray& IntArray::operator=(IntArray&& right) noexcept //移动赋值
{
if(this==&right)
return *this;
arrSize=right.arrSize;
delete ptr;
ptr=right.ptr;
right.arrSize=0;
right.ptr=nullptr; //右值置空,否则将引起同一块内存两次delete
return *this;
}
int& IntArray::get(size_t index)
{
assert(index>=0&&index<arrSize);
return ptr[index];
}
const int& IntArray::get(size_t index) const
{
assert(index>=0&&index<arrSize);
return ptr[index];
}
size_t IntArray::size() const
{
return arrSize;
}
//测试程序
void print(const IntArray& ia)
{
cout<<ia.size()<<" elements: ";
for(size_t i=0;i<ia.size();i++)
{
cout<<ia.get(i)<<" ";
}
cout<<endl;
}
int main()
{
int a[]={1,2,3,4,5};
cout<<"IntArray ia1"<<endl;
IntArray ia1;
cout<<"ia1 ";
print(ia1);
cout<<"IntArray ia2(a,5)"<<endl;
IntArray ia2(a,5);
a[2]=6;
cout<<"ia2 ";
print(ia2);
cout<<"IntArray ia3=ia2"<<endl;
IntArray ia3=ia2;
ia3.get(1)=9;
cout<<"ia3 ";
print(ia3);
cout<<"ia2 ";
print(ia2);
cout<<"ia1=std::move(ia2)"<<endl;
ia1=std::move(ia2);
ia1.get(3)=8;
cout<<"ia1 ";
print(ia1);
cout<<"ia2 ";
print(ia2);
cout<<"IntArray ia4(std::move(ia1))"<<endl;
ia1.get(2)=7;
IntArray ia4(std::move(ia1));
cout<<"ia4 ";
print(ia4);
cout<<"ia1 ";
print(ia1);
cin.get();
}
/*Output*/
/*
IntArray ia1
ia1 0 elements:
IntArray ia2(a,5)
ia2 5 elements: 1 2 3 4 5
IntArray ia3=ia2
ia3 5 elements: 1 9 3 4 5
ia2 5 elements: 1 2 3 4 5
ia1=std::move(ia2)
ia1 5 elements: 1 2 3 8 5
ia2 0 elements:
IntArray ia4(std::move(ia1))
ia4 5 elements: 1 2 7 8 5
ia1 0 elements:
*/
编译器合成的成员函数
只要定义5个拷贝控制成员任何之一,其他的成员都不再自动生成
编译器自动生成的成员函数默认都为public inline
函数
如果不需要编译器合成的拷贝操作, 应该禁止它们。如果只是不定义这些成员函数, 编译器就会在需要时隐式地合成它们。在这种情况下, 应该把构造函数、赋值运算符或其他要禁止的成员函数定义为private
, 从而禁止调用方的代码访问它们。为了防止能访问私有成员的友元函数调用到这些成员, 可以不提供这些成员函数的定义。
在函数参数列表后加=delete
表示禁用函数,且必须出现在函数第一次声明时。不能对析构函数使用=delete
不能删除析构函数。如果析构函数被删除, 就无法销毁此类型的对象。对于删除了析构函数的类型, 不能定义该类型的变量, 也无法释放动态分配的该类型对象的指针。
在函数参数列表后加=default
表示显式地要求编译器产生合成的版本
只能对具有编译器合成版本的成员函数使用=default
即:
· 默认构造函数
· 拷贝构造函数
· 移动构造函数
· 拷贝賦值运算符
· 移动賦值运算符
· 析构函数
引用计数和写时复制技术
#include<cstring>
#include<iostream>
using namespace std;
class str_obj
{ //有引用计数的字符串对象
public:
int len,ref_cnt;
char* s;
str_obj():len(0),ref_cnt(1)
{
s=new char[1];
s[0]=0;
}
str_obj(const char* p):ref_cnt(1)
{
len=strlen(p);
s=new char[len+1];
strcpy(s,p);
}
~str_obj()
{
delete []s;
}
};
class my_string
{
public:
my_string()
{
st=new str_obj;
}
my_string(const char* p)
{
st=new str_obj(p);
}
my_string(const my_string& str) //浅复制,引用计数增加
{
st=str.st;
st->ref_cnt++;
}
~my_string();
my_string&operator=(const my_string& str);
void print() const
{
cout<<st->s;
}
void reverse(); //逆序
private:
str_obj* st;
};
my_string& my_string::operator=(const my_string& str)
{
if(str.st!=st)
{
if(--st->ref_cnt==0)
delete st; //赋值后不再有对象使用st指向str_obj
st=str.st;
st->ref_cnt++;
}
return *this;
}
my_string::~my_string()
{
if(--st->ref_cnt==0) //计数为0时才真正撤销字符串对象
delete st;
}
//写时复制技术
void my_string::reverse()
{
if(st->ref_cnt>1) //对象被多处使用,需要复制一份再修改
{
--st->ref_cnt; //原对象引用计数减1
char* tp=st->s;
st=new str_obj(tp); //新复制,引用计数初始为1
}
if(st->ref_cnt==1) //没有其他对象在使用str_obj
{
int n=st->len;
for(int ix=0;ix<n/2;ix++)
{
char ch=st->s[ix];
st->s[ix]=st->s[n-ix-1];
st->s[n-ix-1]=ch;
}
}
}
int main()
{
my_string str1("Practice makes perfect");
my_string str2;
str2=str1;
cout<<"\nstring1: ";
str1.print();
cout<<"\nstring2: ";
str2.print();
str1.reverse();
cout<<"\nstring1: ";
str1.print();
cout<<"\nstirng2: ";
str2.print();
cin.get();
}
/*Output*/
/*
string1: Practice makes perfect
string2: Practice makes perfect
string1: tcefrep sekam ecitcarP
stirng2: Practice makes perfect
*/
智能指针
C++ 11 标准库提供了智能指针shared_ptr
、unique_ptr
和weak_ptr
. 在<memory>
头文件中定义。shared_ptr
允许多个指针指向同一个对象, shared_ptr
支持的操作如表。
shared_ptr<string>ps;
ps=make_shared<string>("abc");
shared_ptr<int>pi=make_shared<int>(12);
shared_ptr<X>px=make_shared<X>() //px指向默认初始化的X对象
#include<memory>
#include<iostream>
using namespace std;
class HasPtr
{
public:
HasPtr():length(0),ptr(nullptr)
{
cout<<"HasPtr():"<<length<<endl;
}
explicit HasPtr(size_t size):length(size)
{
ptr=new int[size];
cout<<"HasPtr(size_t):"<<length<<endl;
}
~HasPtr()
{
if(!ptr) delete[]ptr;
cout<<"~HasPtr():"<<length<<endl;
}
private:
size_t length=0;
int *ptr=nullptr;
};
//分析hp和sp的不同
int main()
{
freopen("ex.in","r",stdin);
freopen("ex.out","w",stdout);
HasPtr *hp1=new HasPtr(3);
HasPtr *hp2=new HasPtr(4);
delete hp2;
shared_ptr<HasPtr>sp1=make_shared<HasPtr>(5);
shared_ptr<HasPtr>sp2=make_shared<HasPtr>(6);
return 0;
cin.get(); //注意:cin.get()在return 0之前输出会有不同
}
/*Output[FILE]*/
/*
HasPtr(size_t):3
HasPtr(size_t):4
~HasPtr():4
HasPtr(size_t):5
HasPtr(size_t):6
~HasPtr():6
~HasPtr():5
*/
可见hp1未自动销毁,sp1和sp2均自动销毁
智能指针unique_ptr
独自拥有它所指向的对象,操作如下:
与shared_ptr
不同的是,某个时刻只能有一个unique_ptr
指向一个给定对象。当unique_ptr
被销毁时, 它所指向的对象也被销毁。定义unique_ptr
时, 需要将其绑定到一个new 返回的指针上, 必须采用直接初始化的语法。unique_ptr
不支持普通的复制或赋值操作, 只能通过release 或reset 将指针的所有权从一个非const 的unique_ptr
转移给另一个unique_ptr
。
unique_ptr<int>p1;
unique_ptr<int>p2(new int(12));
unique_ptr<string>ps(new string("abc"));
unique_ptr<string>ps1(ps);// 错误,unique不支持复制
unique_ptr<string>ps2;
ps2=ps; // 错误,unique不支持赋值
unique_ptr<string>ps3(ps.release()); //将指针的所有权转移给ps3,ps1置空
unique_ptr<string>ps4(new string("123"));
ps4.reset(ps3.release());// 将所有权从ps3转移给ps4,释放ps4原来所指向的内存
使用智能指针管理动态内存安全方便,但有以下基本规范:
· 不使用相同的内置指针值初始化或reset 多个智能指针。
· 不delete get() 返回的指针。
· 不使用get() 初始化或reset 另一个智能指针。
· 如果使用get() 返回的指针, 记住当最后一个对应的智能指针销毁后, 这个指针就变成无效的。
· 如果使用智能指针管理的资源不是new 分配的内存, 要传递一个删除器函数给它,代替delete 。
智能指针实现引用计数:
#include<cstring>
#include<memory>
#include<iostream>
using namespace std;
class str_obj
{ //有引用计数的字符串对象
public:
int len;
char* s;
str_obj():len(0)
{
s=new char[1];
s[0]=0;
}
str_obj(const char* p)
{
len=strlen(p);
s=new char[len+1];
strcpy(s,p);
}
~str_obj()
{
delete []s;
}
};
class my_string
{
public:
my_string()
{
st=make_shared<str_obj>();
}
my_string(const char* p)
{
st=make_shared<str_obj>(p);
}
my_string(const my_string& str):st(str.st) {} //浅复制,引用计数增加
~my_string() {} //析构函数自动判断智能指针的引用计数,为0时释放对象
my_string& operator=(const my_string& str);
void print()const { cout<<st->s; }
void reverse(); //逆序,改变字符串,写时复制
private:
shared_ptr<str_obj>st;
};
my_string& my_string::operator=(const my_string& str)
{
st=str.st; //递减st的引用计数,递增str.st的引用计数
return *this;
}
void my_string::reverse()
{
if(!st.unique())
{
char *tp=st->s;
st=make_shared<str_obj>(tp); //新创建一个对象返回共享指针
}
if(st.unique())
{
int n=st->len;
for(int ix=0;ix<n/2;ix++)
{
char ch=st->s[ix];
st->s[ix]=st->s[n-ix-1];
st->s[n-ix-1]=ch;
}
}
}
int main()
{
my_string str1("Practice makes perfect");
my_string str2;
str2=str1;
cout<<"\nstring1: ";
str1.print();
cout<<"\nstring2: ";
str2.print();
str1.reverse();
cout<<"\nstring1: ";
str1.print();
cout<<"\nstirng2: ";
str2.print();
cin.get();
return 0;
}
/*Output*/
/*
string1: Practice makes perfect
string2: Practice makes perfect
string1: tcefrep sekam ecitcarP
stirng2: Practice makes perfect
*/
在设计复杂对象时, 尤其是涉及动态内存资源时, 需要决定为对象实现深复制还是浅复制。实现浅复制的动机一般是为了改善性能。但为了保证正确性, 在浅复制时经常会使用引用计数和写时复制技术, 因而增加了复杂度。深复制在开发和维护方面都比浅复制简单, 因此, 在面临选择时, 通常优先选择深复制; 除非证明可行, 才采用浅复制。
另一方面, 优先使用标准库类型如vector 或string 存储复杂对象的数据, 利用智能指针如shared_ptr
或unique_ptr
管理动态内存资源, 效率和正确性都可以得到很大提高。
运算符重载
运算符重载不会改变内置类型表达式中的运算符含义, 只有在至少一个操作数是用户自定义类型的对象时, 才有可能调用该类中重载的运算符。
将运算符函数定义为成员函数时, 调用成员函数的对象( this 指向的对象) 被作为运算符的第一个操作数, 所以对一元运算符函数无须再提供参数。使用成员函数重载二元运算符时, 将当前对象( this 指向的对象) 作为左操作数, 只需要提供一个参数作为右操作数。
如果将运算符承数定义为非成员函数, 重载一元运算符时需要提供一个类类型的参数, 重载二元运算符时需要提供两个参数, 分别作为左、右操作数, 其中至少一个参数必
须是类类型的。由于进行运算时往往要访问类的私有数据, 所以一般将非成员运算符函数声明为类的友元。
除了重载的函数调用运算符operator()
之外, 其他运算符函数不能有默认实参。
对于运算符函数来说, 它或者是类的成员, 或者至少含有一个类类型的参数, 这意味着运算符函数只有在类类型的对象参与运算时才起作用, 当运算符作用于内置类型的运算对象时, 不会改变该运算符原来的含义。
通常情况下,将运算符作用于类型正确的实参时, 会引起重载运算符的调用。也可以像普通函数一样直接调用运算符函数。例如:
//X类的非成员运算符函数operator+
X operator+(const X& left,const X& right) {...}
X data1,data2;
data1+data2;//表达式调用
operator+(data1,data2);//等价的直接函数调用
可以重载的运算符:
定义运算符函数时,要决定是将其声明为类的成员函数还是声明为非成员函数。下面的准则有助于做出抉择
- 赋值
=
、下标[]
、函数调用()
和成员函数访问箭头->
运算符必须是成员函数。 - 复合赋值运算符一般应该是成员, 但并非必须。
- 改变对象状态的运算符或者与给定类型密切相关的运算符, 如自增、自减和解引用运算符, 通常应该是成员。
- 具有对称性的运算符可能转换两个操作数中的任何一个, 如算术、关系和位运算符等, 通常应该是非成员函数。
- 重载移位运算符
<<
和>>
用于对象的I/O 操作时, 左操作数是标准库流对象, 右操作数才是类类型的对象, 只能用非成员函数。
运算符重载虽然为使用自定义类型提供了语法上的方便, 但是不能滥用。只有当用户自定义类型上的操作与内置运算符之间存在逻辑对应关系时, 重载的运算符才能使程序显得更自然、更直观。
//成员函数重载一元运算符
class Byte
{
unsigned char b;
public:
Byte(unsigned char bb=0):b(bb){}
//无副作用的运算符定义为const成员函数
const Byte& operator+() const
{
return *this;
}
const Byte operator-() const
{
return Byte(-b);
}
const Byte operator~() const
{
return Byte(~b);
}
bool operator!() const
{
return !b;
}
//有副作用的运算符定义为非const成员函数
const Byte& operator++()//前缀++
{
b++;
return *this;
}
const Byte operator++(int)//后缀++
{
Byte before(b);
b++;
return before;
}
const Byte& operator--()
{
b--;
return *this;
}
const Byte operator--(int)
{
Byte before(b);
b--;
return before;
}
};
int main()
{
Byte b;
+b; -b; ~b; !b;
++b; b++;
}
//非成员运算符函数重载一元运算符
class Integer
{
long i;
public:
Integer(long ll=0):i(ll){}
//无副作用的运算符参数为const引用
friend const Integer& operator+(const Integer& a);
friend const Integer operator-(const Integer& a);
friend const Integer operator~(const Integer& a);
friend bool operator!(const Integer& a);
//有副作用的运算符参数为非const引用
friend const Integer& operator++(Integer& a);
friend const Integer operator++(Integer& a,int);
friend const Integer& operator--(Integer& a);
friend const Integer operator--(Integer& a,int);
};
const Integer& operator+(const Integer& a) { return a; }
const Integer operator-(const Integer& a) { return Integer(-a.i); }
//const Integer& operator-(const Integer& a) { a.i=-a.i; return a; }
//上面写法错误,原对象改变
const Integer operator~(const Integer& a) { return Integer(~a.i); }
bool operator!(const Integer& a) { return !a.i; }
const Integer& operator++(Integer& a)
{
a.i++;
return a;
}
const Integer operator++(Integer& a,int)
{
Integer before(a.i);
a.i++;
return before;
}
const Integer& operator--(Integer& a)
{
a.i--;
return a;
}
const Integer operator--(Integer& a,int)
{
Integer before(a.i);
a.i--;
return before;
}
+a,-a,~a
不改变对象a,仅表示一个表达式
++a,a++
改变对象a的值,使其自增
//成员函数重载二元运算符
//重载时要带一个参数作为右操作数,当前对象为左操作数
class Byte
{
unsigned char b;
public:
Byte(unsigned char bb=0):b(bb){}
//无副作用的运算符定义为const成员函数,右操作数为const引用
const Byte operator+(const Byte& right) const
{
return Byte(b+right.b);
}
const Byte operator-(const Byte& right) const
{
return Byte(b-b-right.b);
}
const Byte operator*(const Byte& right) const
{
return Byte(b*right.b);
}
const Byte operator/(const Byte& right) const
{
assert(right.b!=0);
return Byte(b/right.b);
}
const Byte operator%(const Byte& right) const
{
assert(right.b!=0);
return Byte(b%right.b);
}
//位运算
const Byte operator^(const Byte& right) const
{
return Byte(b^right.b);
}
const Byte operator&(const Byte& right) const
{
return Byte(b&right.b);
}
const Byte operator|(const Byte& right) const
{
return Byte(b|right.b);
}
const Byte operator<<(const Byte& right) const
{
return Byte(b<<right.b);
}
const Byte operator>>(const Byte& right) const
{
return Byte(b>>right.b);
}
//有副作用的运算符为非const成员函数
//修改并返回左值的运算符:赋值和复合赋值运算
//operator=只能用成员函数重载
Byte& operator=(const Byte& right)
{
if(this==&right) return *this;
b=right.b;
return *this;
}
//复合赋值运算符:+=,-=,*=,/=,%=,^=,&=,\=,<<=,>>=
Byte operator+=(const Byte& right)
{
b+=right.b;
return *this;
}
Byte operator/=(const Byte& right)
{
assert(right.b!=0);
b/=right.b;
return *this;
}
Byte operator^=(const Byte& right)
{
b^=right.b;
return *this;
}
//关系运算符没有副作用,返回bool值
//关系运算符 ==,!=,<,<=,>,>=
bool operator==(const Byte& right) const
{
return b==right.b;
}
bool operator!=(const Byte& right) const
{
return b!=right.b;
}
};
//非成员函数重载二元运算符
//带两个参数,至少一个是类类型的
class Integer
{
long i;
public:
Integer(long ll=0):i(ll){}
//运算结果为新产生的值,按值返回新对象
//算术运算符
friend const Integer operator+(const Integer& left,const Integer& right);
friend const Integer operator-(const Integer& left,const Integer& right);
friend const Integer operator*(const Integer& left,const Integer& right);
friend const Integer operator/(const Integer& left,const Integer& right);
friend const Integer operator%(const Integer& left,const Integer& right);
//位运算符
friend const Integer operator^(const Integer& left,const Integer& right);
friend const Integer operator&(const Integer& left,const Integer& right);
friend const Integer operator|(const Integer& left,const Integer& right);
friend const Integer operator<<(const Integer& left,const Integer& right);
friend const Integer operator>>(const Integer& left,const Integer& right);
//修改并返回左值的复合赋值运算符,第一个参数是非const引用,即左值
//注意:operator=只能用成员函数重载
friend Integer& operator+=(Integer& left,const Integer& right);
friend Integer& operator-=(Integer& left,const Integer& right);
friend Integer& operator*=(Integer& left,const Integer& right);
friend Integer& operator/=(Integer& left,const Integer& right);
friend Integer& operator%=(Integer& left,const Integer& right);
friend Integer& operator^=(Integer& left,const Integer& right);
friend Integer& operator&=(Integer& left,const Integer& right);
friend Integer& operator|=(Integer& left,const Integer& right);
friend Integer& operator>>=(Integer& left,const Integer& right);
friend Integer& operator<<=(Integer& left,const Integer& right);
//关系运算符返回bool值,不改变操作数
friend bool operator==(const Integer& left,const Integer& right);
friend bool operator!=(const Integer& left,const Integer& right);
friend bool operator<(const Integer& left,const Integer& right);
friend bool operator>(const Integer& left,const Integer& right);
friend bool operator<=(const Integer& left,const Integer& right);
friend bool operator>=(const Integer& left,const Integer& right);
};
//算术运算符
const Integer operator+(const Integer& left,const Integer& right)
{
return Integer(left.i+right.i);
}
const Integer operator/(const Integer& left,const Integer& right)
{
assert(right.i!=0);
return Integer(left.i/right.i);
}
//位运算符
const Integer operator&(const Integer& left,const Integer& right)
{
return Integer(left.i&right.i);
}
//复合赋值运算符
Integer& operator+=(Integer& left,const Integer& right)
{
left.i+=right.i;
return left;
}
Integer& operator/=(Integer& left,const Integer& right)
{
assert(right.i!=0);
left.i/=right.i;
return left;
}
//关系运算符
bool operator==(const Integer& left,const Integer& right)
{
return left.i==right.i;
}
运算符函数的参数和返回类型
参数传递和返回方式在选择时要合乎逻辑。
( 1 ) 对于类类型的参数, 如果仅仅只是读参数的值, 而不改变参数, 应该作为const引用
来传递。普通算术运算符和关系运算符都不会改变参数, 所以以const 引用
作为参数传递方式。当运算符函数是类的成员函数时, 就将其定义为const 成员函数。
( 2 ) 返回值的类型取决于运算符的具体含义。如果使用运算符的结果是产生一个新值,就需要产生一个作为返回值的新对象, 通过传值方式返回, 通常由const 限定。如果函数返回的是操作数对象, 则通常以引用方式返回, 根据是否希望对返回的对象进行操作来决定是否返回const 引用。
( 3 ) 所有赋值运算符均改变左值。为了使赋值结果能用于链式表达式, 如a=b-c , 应该返回一个改变了的左值的引用。一般赋值运算符的返回值是非const 引用
, 以便能够对刚刚赋值的对象进行运算。
( 4 ) 关系运算符最好返回bool 值。
返回值优化
通过传值方式返回创建的新对象时, 使用一种特殊的语法
return lnteger(left.i + right.i) ;
这种形式称为临时对象语法, 其行为是创建一个临时的lnteger 对象并返回它。
这种方式和创建并返回一个有名字的对象不同:
Integer temp(left.i+right.i);
return temp;
后一种方式先创建temp 对象, 会调用构造函数: 执return 语句时, 调用拷贝构造函数把temp 复制到外部返回值的存储单元; 最后在temp 离开作用域时调用析构函数。
返回临时对象的方式与此不同。当编译器看到这种语法时, 会明白创建这个对象的目的只是返回它, 所以编译器直接把这个对象创建在外部返回值的存储单元中, 只需要调用一次构造函数, 不需要拷贝构造函数和析构函数的调用。因此, 使用临时对象语法的效率更高, 这种语法也被称为返回值优化。
重载下标运算符
下标运算符作用的对象应该能像数组一样操作,经常用该运算符返回一个元素的引用,以便用作左值。
//为动态数组类vect重载下标运算符
#include<iostream>
#include<cassert>
using namespace std;
class vect
{
public:
explicit vect(int n=10);
vect(const vect& v);
vect(const int a[],int n);
~vect() { delete []p; }
int& operator[](int i);
const int& operator[](int i) const;
int ub()const { return(size-1); }
vect& operator=(const vect& v);
private:
int *p;
int size;
};
vect::vect(int n):size(n)
{
assert(size>0);
p=new int[size];
}
vect::vect(const int a[],int n):size(n)
{
assert(size>0);
p=new int[size];
for(int i=0;i<size;i++)
{
p[i]=a[i];
}
}
vect::vect(const vect& v):size(v.size)
{
p=new int[size];
for(int i=0;i<size;i++)
{
p[i]=v.p[i];
}
}
int& vect::operator[](int i)
{
assert(i>=0&&i<size);
return p[i]; //返回的是左值
}
const int& vect::operator[](int i)const
{
assert(i>=0&&i<size);
return p[i];
}//const成员函数版本
vect& vect::operator=(const vect& v)
{
if(this!=&v)
{
assert(v.size==size); //只允许大小相同的数组赋值
for(int i=0;i<size;i++)
{
p[i]=v.p[i];
}
}
return *this;
}
int main()
{
int a[5]={1,2,3,4,5};
vect v1(a,5);
v1[2]=9;
for(int i=0;i<=v1.ub();i++)
{
cout<<v1[i]<<"\t";
}
cin.get();
}
/*Output*/
/*
1 2 9 4 5
*/
用户定义的类型转换
如果在表达式中使用了类型不合适的操作数, 编译器会尝试执行自动类型转换
重载operator type
运算符可以将当前类型转换为type
指定的类型。这个运算符只能用成员函数重载, 而且不带参数, 它对当前对象实施类型转换操作, 产生type 类型的新对象。
不必指定operator type
函数的返回类型一一返回类型就是type
//编写一个MinInt类,表示100以内非负整数
#include<iostream>
#include<cassert>
using namespace std;
class MinInt
{
char m;
public:
MinInt(int val=0) //int转MinInt
{
assert(val>=0&&val<=100);
m=static_cast<char>(val);
}
operator int() //MinInt转int
{
return static_cast<int>(m);
}
};
int main()
{
MinInt mi(10),num;
num=mi+20; //先将mi转int(自动调用),再整型加法,再将int的30转MinInt
int val=num; //num转int
cout<<mi<<'\t'<<num<<'\t'<<val<<endl;
//num和mi转换为int输出
}
编译器进行的隐式自动类型转换只能调用一个类型转换操作构造函数或operator type
, 而不可能寻找一条潜在的转换路径。如下:
class B{};
class A
{
public:
A(int); //int->A
operator B()const; //A->B
};
void func(B) {}
func(2);//错误,不存在int->B
func(A(2)); //正确,先A(int),编译器再自动调用A::operator B()
C++11引入了显式类型转换运算符explicit
,和显式构造函数一样,编译器不会将一个显式类型转换运算符用于隐式类型转换
class SmallInt
{
char val=0;
public:
SmallInt(char v):val(v) {}
explicit operator int() const { return val; }
};
SmallInt si=3;
si+3;//错误,此处需要隐式类型转换
static_cast<int>(si)+3;//正确:显式类型转换
自动类型转换可能引起的二义性问题
应该在确保不引起二义性, 并且能够优化代码的情况下谨慎使用自动类型转换。最好的办法是保证最多只有一种途径将一个类型转换为另一类型。不要让两个类执行相同的类型转换, 使用显式类型转换运算符, 避免转换目标类型是内置算术类型的类型转换, 这些经验规则对避免类型转换的二义性都有所帮助。
函数调用运算符
类同时能存储状态, 比普通函数更灵活。
函数调用运算符必须是成员函数,一个类可以定义多个不同版本的调用运算符, 但是必须在参数个数或类型上有所区别。
struct AbsInt
{
int operator()(int val) //返回abs(val)
{
return val<0?-val:val;
}
};
AbsInt obj;
int x=obj(-12);
int y=-obj(x);
如果一个类定义了函数调用运算符, 那么该类的对象称为函数对象, 或者仿函数( functor ) 。因为可以调用这种对象, 在代码层面感觉跟函数的使用一样, 但本质上并非函数。
#include<iostream>
using namespace std;
//通过带状态的函数对象,设定不同的税率的计算
class Tax
{
public:
Tax(double r,int b):rate(r),base(b){}
double operator()(double money)
{
return (money-base)*rate;
}
private:
double rate;
int base;
};
int main()
{
Tax high(0.4,30000);
Tax middle(0.25,20000);
cout<<"tax over 3w: "<<high(38900)<<endl;
}
lambda函数
lambda 是函数式编程的概念基础, 函数式编程也是与命令式编程、面向对象编程并列的一种编程范型
典型的lambda 是测试表达式或比较表达式, 可以编写为一条返回语句。这使lambda 简洁、易于理解, 并可自动推断返回类型。
lambda 函数也叫lambda 表达式, 表示一个可调用的代码单元, 可以将其理解为一个未命名的inline 函数。
//lambda函数例子
int main()
{
int girls=3,boys=4;
auto totalChild=[](int x,int y)->int{return x+y};
cout<<totalChild(girls,boys)<<endl;
}
程序中定义了一个lambda 函数, 接受两个参数x 和y , 返回二者的和。与普通函数相比, lambda 函数不需要定义函数名, 取而代之的是多了对方括号[ ]
, 函数的返回值用尾置返回类型的方式声明, 其余跟普通函数定义一样。
lambda函数的定义语法如下:
[capture](parameters)mutable->returntype{statement}
[capture] : 捕捉列表。目是lambda 的引出符, 总是出现在lambda 函数的开头。编泽器据此判断接下来的代码是否是lambda 函数。捕捉列表可以捕捉上下文中的变量以供lambda 函数使用。
[parameters] : 参数列表。如果不需要参数传递, 则可以连同括号()
一起省略。
[mutable] : 可选的修饰符。默认情况下, lambda 函数总是const 函数, mutable 可以取消其常量性。实际上这只是一种语法上的可能性, 现实中用处不多。如果使用mutable , 则不可省略参数列表, 即使参数表为空。
->returntype: 返回类型。用尾置返回类型形式声明, 不需要返回值的时候可以连同符号。一起省略。在返回类型明确的情况下, 也可以省略, 让编译器对返回类型进行推演。
{statement} : 函数体。内容与普通函数一样, 其中可以使用参数, 还可以用捕捉列表中捕获的变量。
//各种lambda函数
[]{}; //最简lambda函数,不能做任何事
int a=3,b=4;
[=] {return a+b;}//省略参数列表和返回类型,可推断返回值int
auto fun1=[&](int c){b=a+c};//无返回值,省略返回类型
auto fun2=[=,&b](int c)->int{return b+=a+c;} //完整的
lambda 函数和普通函数最明显的区别之一, 是lambda 函数可以通过捕捉列表访问上下文中的一些数据。捕捉列表描述了lambda 中可以用哪些上下文数据, 以及使用的方式是值传递还是引用传递。
int main()
{
int girls=3,boys=4;
auto totalChild=[girls,&boys]()->int{return girls+boys;}
}
捕捉列表形式
[var]
: 以值传递方式捕捉变量var
[=]
: 以值传递方式捕捉外围作用域的所有变量( 包括this)
[&var]
: 以引用传递方式捕捉变量var
[&]
: 以引用传递方式捕捉外围作用域的所有变量( 包括this ) 。
[this]
: 以值传递方式捕捉当前的this 指针。
组合形式
: 如[=,&var]
表示以引用传递方式捕捉变量var
, 其余变量值传递。[&,a,b]
表示以值传递方式捕捉a
和b
, 以引用方式捕捉其他变量。使用组合形式时, 要注意捕捉列表不允许变量重复传递, 如[=,var]
就重复值传递了var
,会引起编译错误。
现阶段的编译器通常都会将lambda转化为一个函数对象。因此, 在C++11 中, lambda 函数可以视为函数对象的一种等价形式, 或者是简写的语法形式。因为lambda 函数默认是const 函数, 所以准确地讲, 现有C++11 标准中的lambda 等价的是有const operator()
的函数对象。用户可以使用lambda 代替函数对象来写代码。
标准库定义的函数对象
这些类都被定义为模板的形式, 可以为其指定具体的类型, 作为调用运算符的形参类型。例如,plus<int>
的操作数是int , plus<string>
的操作数是string 。
表示运算符的函数对象常用来替换算法中的默认运算符。例如, 排序算法sort 使用的默认运算符是operator<
, 序列将按升序排列。如果要按降序排列, 可以传入一个greater类型的对象, 该类产生一个调用运算符并负责执行待排序类型的大于比较运算。如:
vector<string>vs={"abc","123","abb","cba"};
sort(vs.begin(),vs.end());
// vs: 123 abb abc cba
sort(vs.begin(),vs.end(),greater<string>());
// vs: cba abc abb 123
标准库函数bind
如果可以对一个对象或表达式使用调用运算符, 则称其为可调用的( callable )
C++语言中有几种可调用对象: 函数、函数指针、函数对象、lambda 函数, 标准库承数bind创建的对象也是可调用的。可调用对象都可以作为参数传递给函数或标准算法。标准厍函数bind 定义在头文件<functional>
中。bind 可以被看作是一个通用的函数适配器, 它接受一个可调用对象, 生成一个新的可调用对象来“ 适应” 算法的参数表。
调用bind 的一般形式为:
auto newCallable=bind(callable,arg_list);
callable 是一个可调用对象,arg_list 是逗号分隔的参数列表, 对应callable 的参数。
newCaIIabIe 是新生成的可调用对象, 当调用newCaIIabIe 时, newCaIIabIe 会调用callable ,并传递arg_list 中的参数。arg_list 中可以出现绑定参数, 也可能包含形为_n
的占位符,表示newCaIIabIe 中的第n 个参数。如_1
是newCaIIabIe 的第一个参数,_2
是newCaIIabIe的第二个参数。
bind 可以绑定可调用对象的参数, 或者重排参数顺序。
#include<iostream>
#include<string>
#include<functional>
using namespace std::placeholders;//定义占位符的命名空间
using namespace std;
void func(string a,int b)
{
cout<<a<<" "<<b<<endl;
}
//bind生成三个新函数,绑定func的参数或者调整参数顺序
auto goo=bind(func,_1,5);
//goo只提供一个参数,作为func第一个参数,func第二个参数绑定为整数5
auto hoo=bind(func,"bindhoo",_1);
//hoo只提供一个参数作为func的第二个参数,func第一个参数绑定为"bindhoo"
auto rfunc=bind(func,_2,_1);
//rfunc提供两个参数,第一个参数为func第二个,第二个为func第一个
int main()
{
func("first",1);
goo("test");
hoo(10);
rfunc(4,"last");
}
任何类别的可调用对象都可以传递给算法。如果算法需要的函数功能和func 定义的相同, 但是参数出现的顺序和func 的定义不同, 就可以用bind 生成一个调用func 的新函数如rfunc, 来适应这种情况下的需要。
见下篇…
由于写的越多Markdown在线编辑器越卡,所以后半部分另开一篇。。。