文章目录
小点
- 数据成员类内初始值只能放在等号
=
右边, 或者放在花括号{ }
里, 不能使用圆括号()
。 - 成员函数的定义虽然处于类定义的花括号之外, 但还是在类作用域内,所以可以自由访问类的成员, 不需要成员访问语法。
成员函数在类内和类外定义略有差别: 在类定义的花括号内定义的成员函数默认为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(