养成良好的类设计习惯
目录
5.3 静态成员说明-static
一、规划好成员变量
1.1 确保成员变量的封装性
如果不是c语言的项目开发,尽量请不要采用struct来组织您的类,因为这样会让您的成员变量失去封装性。如果使数据成员为 public,每个人都可以对它读写。
将成员变量放置private访问权限内,对外提供这些成员变量的只读、只写、读写访问是个良好的习惯。
class Commit_Access {
public:
int getreadonly() const{ return readonly; }
void setreadwrite(int value) { readwrite = value; }
int getreadwrite() const { return readwrite; }
void setwriteonly(int value) { writeonly = value; }
private:
int noaccess; // 纯内部数据,禁止访问这个 int
int readonly; // 可以只读这个 int
int readwrite; // 可以读/写这个 int
int writeonly; // 可以只写这个 int
};
1.2 与派生类共享变量
对于需要派生类继承的成员变量,放置在protected访问权限内,只提供给派生类共享使用:
class Commit_Access {
public:
int getderivedataonly(){return derivedata;}
protected:
int derivedata; //子类可用
private:
};
class Derived : public Commit_Access
{
private:
/* data */
int indata;
public:
void setpdataonly( int val ){derivedata=val;} //
};
1.3 与普通函数或其他类成员函数共享变量
对于确实需要使用类内部成员变量的普通函数或其他类成员函数,请将它们声明为友元。如果其他类没有大部分的成员函数需要使用该类内部成员变量的话,建议不要将类声明为友元,而是将需要使用内部成员变量的其他类成员函数单独声明为友元。
//test1.h
#include <ostream>
class PTest;
class FriendTest
{
private:
/* data */
public:
void doit();
void dosomething(PTest* const obj);
};
class PTest
{
private:
/* data */
int val;
public:
friend std::ostream& operator<<(std::ostream& os, const PTest& obj);
friend void FriendTest::doit(); //针对成员函数友元而非类
friend void FriendTest::dosomething(PTest* const obj);
};
//test1.cpp
std::ostream& operator<<(std::ostream& os, const PTest& obj)
{
os << obj.val;
return os;
}
void FriendTest::doit()
{
PTest obj;
++obj.val;
};
void FriendTest::dosomething(PTest* const obj)
{
(obj->val)+=10;
};
1.4 为成员变量做声明次序
如果类中存在普通成员变量和动态成员变量(指针变量、动态内存分配),在声明次序中,请将普通成员变量声明放置前面,将动态成员变量放置后面,便于后面初始化列表。对于普通成员变量中,静态成员变量、const成员变量在前,随后跟进内置类型变量,才接着自定义类类型成员变量。对于动态指针变量,也是类似,内置类型指针变量在前,自定义类类型指针变量在后。
class A
{
private:
int val;
public:
};
class B{
private:
int val;
public:
};
class C
{
private:
static int si;
const char cc = 'D';
int ival;
double dval;
A a;
char *pc;
B *pb;
public:
};
对于同级类型的成员变量,最好是短长度类类型在前,长长度类类型在后,无论是从内存对齐角度,还是从构造初始化效率上都有好处:
class D
{
private:
bool b1;
char c1;
short s1;
int ival;
double dval;
int vec[5];
public:
};
/*不良习惯
class D
{
private:
double dval;
bool b1;
int vec[5];
char c1;
int ival;
short s1;
public:
};
*/
1.5 为成员变量做归集
对于一些密切相关的变量,除了放置一起便于逻辑理解外,最好是归并出一个子对象,让这些变量和其行为聚在一起。例如下面例子,一个双端队列和一个线程锁作为网络读取信息接口的成员变量时,完全可以将它们抽离出来,作为一个缓存队列类,让该类对数据入列、出列、容量大小等行为负责,网络读取信息接口类只采用该缓存队列对象即可:
#include <queue>
#include <mutex>
class CacheQue
{
private:
std::deque<std::string> msgque;
std::mutex msgmutex;
public:
//func
};
class ReadFromNet
{
private:
// std::deque<std::string> msgque;
// std::mutex msgmutex;
//other data
int readflag;
int ival;
//...
CacheQue msgs;
public:
//func
};
二、构造函数和析构函数
2.1 构造函数和析构函数默认还是主动创建。
几乎所有的类都有一个或多个构造函数,一个析构函数,无论是显式创建,还是编译器默认创建。因为它们提供了最基本的功能。构造函数控制对象生成时的基本操作,并保证对象被初始化;析构函数摧毁一个对象并保证它被彻底清除。
通常是对那些包含c++标准内置类型的普通成员变量才采取隐式构造函数支持,例如各种原子类型、容器等。
class Obj1
{
private:
/* data */
int id;
double val;
std::vector<int> vec; //OK
std::string str; //OK
public:
//default func
};
例如类类型成员变量是一个动态句柄,隐式定义的析构函数是不会正确实现类类型设计者的想法,这就需要手动定义析构函数。若某个类需要用户定义的析构函数,也需要用户起码定义一个构造函数。
class Obj2
{
private:
/* data */
char* pstr;
public:
Obj2(){pstr = (char*)malloc(10*sizeof(char));}//手动显式构造函数
virtual ~Obj2(){delete[] pstr; pstr=nullptr;} //手动显式析构函数
//default func
};
如果成员变量中有动态句柄的,最好给析构函数声明为virtual虚函数,防止被使用者继承使用时,派生类无需主动释放基类内存。
2.2 主动显式定义构造函数及析构函数要求
另外自定义类类型包含以下成员变量时,也需要显式定义构造及析构函数:
- 类型T 拥有具有 const 限定的非类类型(或其数组)的非静态数据成员;
- 类型T 拥有引用类型的非静态数据成员;
- 类型T 拥有无法复制赋值的非静态数据成员,直接基类或虚基类;
- 类型T 是联合体式的类,且拥有的某个变体成员对应的复制赋值运算符是非平凡的。
class Obj3{ private: int ival;};
class Base1{
public:
Base1(){ ptr = new Obj3();};
virtual ~Base1(){ if(nullptr!=ptr){delete ptr; ptr = nullptr;}};
private:
Obj3 *ptr;
};
class Obj4 : public Base1
{
public:
//...other
Obj4(): Base1(),val(0){ ptr = new Obj3(); }; //构造函数
~Obj4(){if(nullptr!=ptr){delete ptr; ptr = nullptr;}};//析构函数
private:
const int a = 10; //
int val;
Obj3 *ptr; //
};
另外,如果某类型需要作为基类使用,哪怕没有动态内存分配的成员变量,最好也将析构函数声明为virtual虚函数。关于更多虚函数与类的设计要求,请参考本专栏前一篇(篇三)博文。
2.3 析构函数按变量声明逆序释放
析构函数释放动态成员变量内存时,最好按照成员变量声明次序的逆序来释放:
class Base1{
public:
Base1(){
ptr = new Obj3();
pc = new char[10];
};
virtual ~Base1(){
if(nullptr!=pc){delete[] pc; pc = nullptr;}
if(nullptr!=ptr){delete ptr; ptr = nullptr;}
};
private:
Obj3 *ptr;
char *pc;
};
2.4 建议采用初始化列表初始化成员变量
尽量使用初始化而不要在构造函数里赋值:
const 和引用数据成员只能用初始化,不能被赋值。
指针成员的对象在进行拷贝和赋值操作时可能会引起指针混乱,或存在赋值禁止情况。
用成员初始化列表初始化成员变量比在构造函数里赋值初始化成员变量更有效率,因为用成员初始化列表时,只有一个成员函数被调用。而在构造函数里赋值时,将有两个被调用。
class Base2{
public:
Base2(int val){ ptr = new int(val);};
virtual ~Base2(){ if(nullptr!=ptr){delete ptr; ptr = nullptr;}};
private:
Base2(const Base2&) = delete;
Base2& operator=(const Base2&) = delete;
int *ptr;
};
class Obj5
{
public:
Obj5(const int &ival_, Base2* pc_) : icval(10),ival(ival_),pc(pc_){};
virtual ~Obj5(){ if(nullptr!=pc){delete pc; pc=nullptr;}};
private:
const int icval;//必须通过成员初始化列表进行初始化
int ival;
const Base2* pc;//必须通过成员初始化列表进行初始化
};
即使是一个很简单的类型,不必要的函数调用重复多了,也会造成很高的代价。尤其是自定义类型,随着业务扩充类变得越来越大,越来越复杂,它们的构造函数也越来越大而复杂,那么对象创建的代价也越来越高。养成尽可能使用成员初始化列表的习惯,不但可以满足 const 和引用成员初始化的要求,还可以大大减少低效地初始化数据成员的机会。总之,通过成员初始化列表来进行初始化总是合法的,效率也决不低于在构造函数体内赋值,它只会更高效。
2.5 按声明次序初始化变量
类成员是按照它们在类里被声明的顺序进行初始化的,和它们在成员初始化列表中列出的顺序没一点关系。另外,基类数据成员总是在派生类数据成员之前被初始化,所以使用继承时,要把基类的初始化列在成员初始化列表的最前面。
class Base2{
public:
Base2(int val){ ptr = new int(val);};
virtual ~Base2(){ if(nullptr!=ptr){delete ptr; ptr = nullptr;}};
private:
Base2(const Base2&) = delete;
Base2& operator=(const Base2&) = delete;
int *ptr;
};
class Obj6 : public Base2
{
public:
Obj6(const bool& bflag_, const int& ival_, const double& dval_,const int& size=10)
: Base2(1),bflag(bflag_), ival(ival_), dval(dval_)/*, pc(nullptr)*/
{
pc = (char*)malloc(size*sizeof(char));
};
virtual ~Obj6(){ if(nullptr!=pc){delete pc; pc=nullptr;}};
private:
bool bflag;
int ival;
double dval;
char* pc;
};
class Obj7 : public Base2
{
public:
Obj7(const bool& bflag_, const int& ival_, const double& dval_,const int& size=10)
: ival(ival_), Base2(1), dval(dval_),bflag(bflag_)/*, pc(nullptr)*/
{
pc = (char*)malloc(size*sizeof(char));
};
virtual ~Obj7(){ if(nullptr!=pc){delete pc; pc=nullptr;}};
private:
bool bflag;
int ival;
double dval;
char* pc;
};
//
#include <iostream>
#include <chrono>
const unsigned long sizel = 10000;
void test1(void)
{
auto start = std::chrono::system_clock::now();
for (size_t row = 0; row < sizel; row++)
for (size_t i = 0; i < sizel; i++)
{
Obj6 obj6(i/2,i%2,i*1.0);
}
auto end = std::chrono::system_clock::now();
std::chrono::duration<double,std::milli> diff = end-start;
std::cout << "Obj6 diff.count() = " << diff.count() << "ms\n";
start = std::chrono::system_clock::now();
for (size_t row = 0; row < sizel; row++)
for (size_t i = 0; i < sizel; i++)
{
Obj7 obj7(i/2,i%2,i*1.0);
}
end = std::chrono::system_clock::now();
diff = end-start;
std::cout << "Obj7 diff.count() = " << diff.count() << "ms\n";
};
//
D:\workForMy\workspace\class_test4_c++>test.exe
Obj6 diff.count() = 13927.5ms
Obj7 diff.count() = 13970.9ms
上述代码中,Base2总会被首先初始化(当然也可隐式初始化),然后是 bflag、iva、dvall和 pc。对一个对象的所有成员来说,它们的析构函数被调用的顺序总是和它们在构造函数里被创建的顺序相反。那么,如果不按声明次序初始化,编译器就要为每一个对象跟踪其成员初始化的顺序,以保证它们的析构函数以正确的顺序被调用。这会带来昂贵的开销。所以,为了避免这一开销,编译器就会在同一种类型的所有对象在创建(构造)和摧毁(析构)过程中对成员的处理顺序都是相同的,而不管成员在初始化列表中的顺序如何。
再看下面示例,先将size初始化,再用size去初始化vector,却没有得到想要结果。
#include <vector>
#include <iostream>
class Obj8
{
public:
Obj8(int size_) :size(size_), ivec(size){};
~Obj8(){};
void test(){
std::cout << "size = "<<size<<"\n";
std::cout << "ivec.size = "<<ivec.size()<<"\n";
}
private:
std::vector<int> ivec;
int size;
};
void test1(void)
{
Obj8 obj8(10);
obj8.test();
};
//out log
size = 10
ivec.size = 6290744 //为啥不是10呢
这是因为类成员是按照它们在类里被声明的顺序进行初始化的,和它们在成员初始化列表中列出的顺序没一点关系,即vector先初始化,传入了一个不确定的数值,因此vector并没有等到size先初始化再初始化,而是它比size先初始化化了。上述代码只有交换两个成员的声明次序才是正确的。
//交换声明次序,其他不变
int size;
std::vector<int> ivec;
//out log
size = 10
ivec.size = 10
三、拷贝构造和赋值操作符
3.1 默认拷贝构造和赋值操作
对于成员变量是内置类型的非动态变量,虽然可以采用默认拷贝构造函数和复制赋值函数,但还是建议采用default关键词显式标注:
#include <string>
class Obj9
{
public:
Obj9() = default;
Obj9(const Obj9&) = default;
Obj9& operator=(const Obj9&) = default;
private:
bool bval;
int ival;
std::string str;
};
//
Obj9 Obj9_1;
Obj9 Obj9_2(Obj9_1);
Obj9 Obj9_3;
Obj9_2 = Obj9_1;
3.2 自定义拷贝构造和赋值操作
为需要动态分配内存的类显式声明一个拷贝构造函数和一个赋值操作符或明确指出禁止拷贝和赋值。
因为对于动态分配内存的类,如果没有明确定义或禁止拷贝构造函数和赋值操作符,在使用调用时,例如类有一个char*变量,使用operator=,c++会生成并调用一个缺省的 operator=操作符。这个缺省的赋值操作符会执行从 a 的成员到 b 的成员的逐个成员的赋值操作,对指针(a.data 和 b.data) 来说就是逐位拷贝。这就有可能出现,b 曾指向的内存永远不会被删除,因而会永远丢失,从而产生内存泄漏。又或现在 a 和 b 包含的指针指向同一个字符串,那么只要其中一个离开了它的生存空间,其析构函数就会删除掉另一个指针还指向的那块内存。第二种异常情况在使用拷贝构造函数时同会出现。
//test1.h
class MyString
{
public:
MyString(void); //默认构造函数
MyString( const char *str = nullptr ); //普通构造函数
MyString( const MyString &other ); //拷贝构造函数
~MyString( void ); //析构函数
MyString& operator=(const MyString &other); //赋值函数
MyString& operator=(const char* other); //赋值函数
char* c_str(void) const; //取值(取值)
private:
void init(const char *str);
void copy(const char *str);
private:
char *m_data;
};
//test1.cpp
#include <cstring>
//默认构造函数
MyString::MyString(void)
{
MyString(nullptr); //内部调用普通构造函数
}
//普通构造函数
MyString::MyString(const char *str)
{
init(str);
}
// MyString 的析构函数
MyString::~MyString(void)
{
delete [] m_data; // 或 delete m_data;
}
void MyString::init(const char *str)
{
if(nullptr==str)
{
m_data = new char[1]; //对空字符串自动申请存放结束标志'\0'的空
*m_data = '\0';
}else{
int length = strlen(str);
m_data = new char[length+1]; // 分配内存
strcpy(m_data, str);
}
};
void MyString::copy(const char *str)
{
if(nullptr!=str)
{
delete [] m_data; //释放原有的内存资源
int length = strlen( str );
m_data = new char[length+1]; //重新分配内存
strcpy( m_data, str);
}
};
//拷贝构造函数
MyString::MyString( const MyString &other ) //输入参数为const型
{
init(other.m_data);
}
//赋值函数
MyString &MyString::operator =( const MyString &other )//输入参数为const型
{
if(this == &other) //检查自赋值
return *this;
copy(other.m_data);
return *this; //返回本对象的引用
}
MyString& MyString::operator=(const char* other) //赋值函数
{
copy(other);
return *this; //返回本对象的引用
};
char* MyString::c_str(void) const
{
return (char*)m_data;
}
//
MyString mstr1("hello");
MyString mstr2("");
mstr2 = mstr1;
MyString mstr3 = mstr2;
mstr3 = "world";
3.3 禁止拷贝构造和赋值操作
如果不需要禁止拷贝构造和复制赋值函数,请明确显式禁止它们,可以采用以下方式:
- 将拷贝构造和复制赋值函数声明为私有函数,只有声明了友元的函数或内部成员函数可以使用。
- 将拷贝构造和复制赋值函数声明为私有函数,并空定义,虽然可以调用,但无效。
- 将拷贝构造和复制赋值函数声明为私有函数并采用关键词delete显式删除,任何调用在编译报警。
class Obj10
{
public:
Obj10()= default;
private:
//方式一,自定义提供拷贝构造及复制赋值
// Obj10(const Obj10&){/*code*/};
// Obj10& operator=(const Obj10&){/*code*/return *this;};
//方式二,提供默认拷贝构造及复制赋值
// Obj10(const Obj10&) =default;
// Obj10& operator=(const Obj10&) = default;
//方式三,自定义提供空的拷贝构造及复制赋值
// Obj10(const Obj10&){/*不做任何处理*/};
// Obj10& operator=(const Obj10&){/*不做任何处理*/return *this;};
//方式四,强制删除拷贝构造及复制赋值,禁止任何函数调用
Obj10(const Obj10&) = delete;
Obj10& operator=(const Obj10&) = delete;
private:
bool bval;
int ival;
std::string str;
char* pc;
};
3.4 确保所用成员变量赋值,赋值操作返回左参引用
因为赋值运算符的结合性是由右向左,所以operator=的返回值必须可以作为一个输入参数被函数自己接受,遵循 operator=输入和返回的都是类对象的引用的原则。当定义自己的赋值运算符时,必须返回赋值运算符左边参数的引用"*this"。如果不这样做,就会导致不能连续赋值,或导致调用时的隐式类型转换不能进行,或两种情况同时发生。
同时需要注意的是对拷贝构造函数和赋值操作符进行定义时,确保对所有数据成员实现了赋值,尤其是当类里增加新的数据成员时,也要记住更新拷贝构造函数和赋值操作符函数。
当使用者调用类型时,实现自己给自己赋值的情况(obj1=obj1,obj1=obj2但obj2是obj1的别名),因此需要确保在 operator=函数中检查给自己赋值的情况。因为一个赋值运算符必须首先释放掉一个对象的资源(去掉旧值),然后根据新值分配新的资源。在自己给自己赋值的情况下,释放旧的资源将是灾难性的,因为在分配新的资源时会需要旧的资源。
class Obj11
{
public:
Obj11(const int& ival_, const double& dval_=0.0)
: ival(ival_),dval(dval_){ };
Obj11(const Obj11& obj){
ival = obj.ival;
dval = obj.dval;
};
Obj11& operator=(const Obj11& obj){
if(this==&obj) return *this;
ival = obj.ival;
dval = obj.dval;
return *this;
};
private:
int ival;
double dval;
};
//
Obj11 Obj11_1(1), Obj11_2(2), Obj11_3(3), Obj11_4(4);
Obj11_1 = Obj11_2 = Obj11_3 = Obj11_4;
(Obj11_1 = Obj11_2) = Obj11_3 = Obj11_4;
Obj11_1 = (Obj11_2 = Obj11_3) = Obj11_4;
定义Obj11类型的 operator=返回引用"*this",该对象可以类似内置类型一样,实现连续(链式)赋值操作。
四、类成员函数行为定义
4.1 不要急于增加行为函数
通常类的成员函数就应用场景来划分,为类基本成员函数及类行为成员函数。
类基本成员函数就是前面讨论过的最基本的构造函数、析构函数、拷贝构造函数、复制赋值函数;类行为成员函数主要是依据类型成员变量就业务应用提供的特有行为实现,例如各种操作符函数如移动拷贝构造、移动赋值、类调用函数、一般赋值操作符、算术操作符、IO操作符等,以及查找、排序、字符操作、图元处理等复杂行为实现。
一个类最初设计的成员函数应该是包含默认隐式定义或显式定义的构造函数(一个或多个)、一个析构函数,明确的使用还是禁止的拷贝构造函数、复制赋值操作符。
例如前面定义的MyString类,实现了两个构造函数,一个析构函数,一个拷贝构造函数,两个赋值操作符函数,提供类内置的功能函数来辅助构造及赋值操作函数,最后提供了一个获取私有变量的取值函数:
class MyString
{
public:
MyString(void); //无参数构造函数,通过调用普通构造函数来实现的
MyString( const char *str = nullptr ); //普通构造函数,通过调用了内置init函数实现
MyString( const MyString &other ); //拷贝构造函数,也调用了内置init函数实现
~MyString( void ); //一个析构函数
MyString& operator=(const MyString &other); //拷贝赋值函数,MyString到MyString,调用了内置copy函数实现
MyString& operator=(const char* other); //转换赋值函数,char*到MyString,调用了内置copy函数实现
char* c_str(void) const; //取值(取值)
private:
void init(const char *str);
void copy(const char *str);
private:
char *m_data;
};
上述代码中,MyString类型接口支持使用者实现以空形参或字符指针创建对象、删除对象、对象拷贝,以及字符指针转型为对象、取得字符变量指针来使用存储内容。
类的设计应该遵循按需逐步添加行为成员函数,提供满足当前场景业务想做的任何合理的事情的接口,并坚持函数尽可能少、每两个函数都没有重叠功能的接口。
在接口里新增加一个函数时要仔细考虑:在接口完整的前提下去增加一个新函数,它所带来的方便性是否超过它所带来的额外代价,如复杂性,可读性,可维护性和编译时间等。例如一个通用的功能用成员函数实现起来会更高效,这将是把它增加到接口中的好理由。又列入增加一个成员函数可以防止用户错误,也是把它加入到接口中的有力依据。
4.2 为新增行为做好规划
新增加一个行为是,需要明确是以成员函数、普通函数、还是友元函数方式提供,如果作为成员函数,需要提供怎样的访问权限,是否需要声明为虚函数。
以应用需求场景的方式推导新增行为的定位是个良好的习惯。例如还是以MyString为例,如果需要支持输出到ostream上显示,如果将"operator<<"定位为成员函数,虽然也能实现输出到ostream上显示的效果,但是使用习惯却和标准库使用不一致:一个是内容"hello"输出到ostream上,一个是ostream输出内容"hello"。
class MyString
{
public:
//other
std::ostream& operator<<(std::ostream& output){
output << std::string(m_data);
return output;
};
//other
private:
char *m_data;
};
//
MyString mstr1("hello");
mstr1 << std::cout; //OK
// std::cout << mstr1; //error
//out log
hello
而将operator<<定位为普通函数,给与友元支持,那么其输出习惯就标准库保持一致,并从行为上ostream输出内容"hello"也更贴切实际。
class MyString
{
public:
//other
friend std::ostream& operator<<(std::ostream& output,const MyString& obj){
output << std::string(obj.m_data);
return output;
};
//other
private:
char *m_data;
};
//
MyString mstr1("hello");
//mstr1 << std::cout; //
std::cout << mstr1; //OK
//out log
hello
4.3 谨慎重载成员函数
当成员函数名称相同、形参数量相同,实现内容几乎一致时,请考虑默认实参、类型转换是否可以满足要求,以节省冗长的重载设计。
class Obj12
{
public:
void func(){ std::cout<<"func()\n"; };
void func(const char& sval_){std::cout<<"func(const char&)\n"; };
void func(const int& ival_){std::cout<<"func(const int&)\n"; };
void g(const int& ival_=0){std::cout<<"g(const int&)\n";};
private:
int ival;
};
//
Obj12 obj12;
obj12.func();
obj12.func('a');
obj12.func(0);
obj12.func(10);
obj12.g();
obj12.g('a');
obj12.g(0);
obj12.g(10);
//out log
func()
func(const char&)
func(const int&)
func(const int&)
g(const int&)
g(const int&)
g(const int&)
g(const int&)
4.4 谨慎继承体系下的成员冲突
在多重继承体系中,尤其是菱形继承体系下,往往存在成员冲突问题,需要谨慎设计类的继承方法、成员定义等。需要特别考虑多重继承与虚函数结合情况,这方面相关请参考本专栏的本主题的篇三博文“类与虚函数”,这里就过多展开阐述。
class Base3 {
public:
int doIt(){return 3;};// 一个叫做int doIt 的函数
};
class Base4
{
public:
void doIt(){};// 一个叫做void doIt 的函数
};
class Derived1: public Base3, public Base4 { public: };
class Derived2: public Base3{};
class Derived3: public Base3{};
class Derived4: public Derived2,public Derived3{};
//
Derived1 d1;
// d1.doIt(); // 错误!——二义
// int i1 = d1.doIt();// 错误!——二义
Derived4 d4;
// d4.doIt(); // 错误!——二义
很多时候,不少类设计出来了,在没使用上碰上二义的情况下,程序可以使用。这潜在的二义性就不能及时发现,它可以长时期地潜伏在程序里,不被发觉也不活动,直到某个关键时刻来一下。
五、类成员约束
5.1函数与const运用
const 关键字在类中应用广泛。在类的内部,它可以用于静态和非静态成员。对指针来说,可以指定指针本身为 const,也可以指定指针所指的数据为 const,或二者同时指定为 const。在一个函数声明中,const 可以指的是函数的返回值,或某个参数;对于成员函数,还可以指的是整个函数。
我们通常使用const加上“传引用”而不是“传值”方式来定义成员函数形参。通过值来传递一个 对象最终导致调用了一个对象类型拷贝构造函数,而通过引用来传递参数避免了构造新的参数副本,节省了开销。另外,当一个派生类的对象作为基类对象被值传递时,它(派生类对象)的作为派生类所具有的行为特性会被“切割”掉,从而变成了一个简单的基类对象,使用引用传递可以避免切割问题。
class X
{
public:
int getVal(void) const{return ival;}
void setVal(const int& ival_){
ival=ival_;
// ival_ += 1; //error
};
friend const X operator+(const X& lhs,const X& rhs){
X ret(lhs);
ret.ival+=rhs.ival;
return ret;
};
friend X operator-(const X& lhs,const X& rhs){
X ret(lhs);
ret.ival-=rhs.ival;
return ret;
};
private:
int ival;
};
//
X x1,x2;
x1.setVal(10);
x2.setVal(20);
(x1-x2)=x1; //OK
//(x1+x2)=x1; //error
x1=x1+x2; //OK
通常使用 const 约束成员函数的返回不能被修改。任何不会修改数据成员的函数都应该声明为const类型。如果在编写const成员函数时,不慎修改了数据成员,或者调用了其它非const成员函数,编译器将指出错误,这无疑会提高程序的健壮性。const 成员函数的const声明,const关键字只能放在函数声明的尾部,对于普通函数、友元函数,const关键字放在函数声明的前部。
在 const成员函数中,this的类型是一个指向 const类类型对象的 const指针。既不能改变 this所指向的对象,也不能改变 this所保存的地址。如果给以“指针传递”方式的函数返回值加const修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const修饰的同类型指针。const放在函数后主要是限制类中的成员函数,const放在函数前是限制函数返回类型为指针时通过指针对返回值的修改。
通过 const,可以通知编译器和其他程序员某个值要保持不变。只要是这种情况,你就要明确地使用 const ,因为这样做就可以借助编译器的帮助确保这种约束不被破坏。然而有时应用const约束返回也有失算的时候。一些不遵守 const 约束定义的成员函数也可以通过编译测试。例如下面示例,一个“修改了指针所指向的数据(成员函数operator char*() const)”的执行语句,其行为显然违反了 const约束(pchar1[0] = 'a';),但编译时会通过。
class MyString
{
public:
//other
operator char*() const; //MyString转char*
char* c_str(void) const; //取值(取值)
//other
private:
char *m_data;
};
//test1.cpp
MyString::operator char*() const
{
return (char*)m_data;
};
char* MyString::c_str(void) const
{
return (char*)m_data;
};
//
MyString mstr1("hello\n");
std::cout << mstr1; //OK
char *pchar1 = mstr1; //operator char*() const
pchar1[0] = 'a';
pchar1 = nullptr;
std::cout << mstr1; //OK,输出aello
char *pchar2 = mstr1.c_str(); //
// pchar2[0] = "b"; //error,不能将 "const char *" 类型的值分配到 "char" 类型的实体
std::cout << mstr1; //OK
当然,也不要因为函数返回引用的一些优点而放弃返回对象,必须返回一个对象时不要试图返回一个引用。通常对于成员函数,大多采用返回引用的方式,而对于友元函数,多数采用返回对象来实现。
Obj& Obj::operator-=(const Obj& rhs)
{
ival -= rhs.ival;
return *this;
}
Obj operator-(const Obj& lhs)
{
Objret(lhs);
ret.ival = -ret.ival;
return ret;
}
5.2 inline
说明符与成员变量
inline 说明符的目的是提示编译器做优化,譬如函数内联,这通常要求编译方能见到函数的定义。编译器能(并且经常)就优化的目的忽略 inline 说明符的存在与否。若编译器进行函数内联,则它会以函数体取代所有对它的调用,以避免函数调用的开销(将数据置于栈上和取得结果),这可能会生成更大的可执行文件,因为函数可能会被重复多次。结果同仿函数宏,只是用于该函数的标识符和宏指代可见于定义点的定义,而不指代调用点的定义。
*inline 函数说明符,在用于函数的声明说明符序列时,将函数声明为一个内联(inline)函数。
1)完全在 class/struct/union 的定义之内定义,且被附着到全局模块 (C++20 起)的函数是隐式的
内联函数,无论它是成员函数还是非成员 friend 函数。
2)(C++11 起)声明有 constexpr 的函数是隐式的内联函数。弃置的函数是隐式的内联函数:其(弃置)
定义可出现在多于一个翻译单元中。
3)(C++17 起)inline 说明符,在用于具有静态存储期的变量(静态类成员或命名空间作用域变量)的
声明说明符序列时,将变量声明为内联变量。声明为 constexpr 的静态成员变量(但不是命名空间
作用域变量)是隐式的内联变量。
在内联函数中,
*所有函数定义中的函数局部静态对象在所有翻译单元间共享。
*所有函数定义中所定义的类型同样在所有翻译单元中相同。
(C++17 起) 关键词 inline 对于函数的含义已经变为“容许多次定义”而不是“优先内联”,
因此这个含义也扩展到了变量。
在 C++ 中,若函数声明为内联,则它必须在每一个翻译单元声明为内联,而且每一个内联函数都必须有准确相同的定义)。另一方面, C++ 允许非 const 的函数局域 static 对象,而且所有来自一个内联函数不同定义版本的函数局部 static 对象都相同。隐式生成的成员函数和任何在其首条声明中声明为预置的成员函数,与任何其他在类定义内定义的函数一样是内联的。
class InlineTest
{
public:
void inl(int& ival_){}; //inline隐式
inline void f(int& ival_){}; //inline显式
void g(char& cval_);
constexpr void func(double& dval);//inline隐式,(C++11 起)
private:
inline static int n = 1; //inline变量
};
//test1.cpp
inline void InlineTest::g(char& cval_){}; //inline显式
constexpr void InlineTest::func(double& dval){};//inline隐式
不管是否进行内联,内联函数都保证下列语义:任何拥有内部链接的函数都可以声明成 static inline ,没有其他限制。一个非 static 的内联函数不能定义一个非 const 的函数局部 static 对象,并且不能使用文件作用域的 static 对象。
5.3 静态成员
说明-static
类的静态成员不与类的对象关联:它们是具有静态或线程 (C++11 起)存储期的独立变量,或者常规函数。static 关键词只会用于静态成员在类定义中的声明,而不用于该静态成员的定义。
class X { private:static int n; }; // 声明(用 'static')
int X::n = 1; // 定义(不用 'static')
静态成员函数:
- 静态成员函数不关联到任何对象。调用时,它们没有 this 指针。
- 静态成员函数不能是 virtual、const 或 volatile 的。
- 静态成员函数的地址可以存储在常规的函数指针中,但不能存储在成员函数指针中。
静态数据成员:
- 静态数据成员不关联到任何对象。即使不定义类的任何对象它们也存在。整个程序中只有一个拥有静态存储期的静态数据成员实例,除非使用关键词 thread_local,此时每个线程都有一个具有线程存储期的该对象 (C++11 起)。
- 静态数据成员不能是 mutable 的。
- 在命名空间作用域中,如果类自身具有外部连接(即不是无名命名空间的成员),那么类的静态数据成员也具有外部连接。局部类(定义于函数内部的类)和无名类,包括无名类的成员类,不能拥有静态数据成员。
- 静态数据成员可以声明为 inline。 inline 静态数据成员可以在类定义中定义,而且可以指定初始化器。
- 如果整型或枚举类型的静态数据成员被声明为 const(且非 volatile),那么它能以其中的每个表达式均为常量表达式的初始化器直接在类定义内初始化
- 如果声明字面类型 (LiteralType) 的静态数据成员为 constexpr,那么它必须以其中的每个表达式均为常量表达式的初始化器直接在类定义内初始化
- 如果 const 非 inline (C++17 起)静态数据成员或 constexpr 静态数据成员 (C++11 起)(C++17 前)被 ODR 式使用,那么仍然需要命名空间作用域的定义,但它不能有初始化器。可以提供定义,尽管这是冗余的 (C++17 起)。
- 如果静态数据成员被声明为 constexpr,那么它隐含为 inline 且不必在命名空间作用域重声明。这种无初始化器的重声明(之前则需要,如上所示)仍然得到容许,但已被弃用(C++17 起)。
class Obj13
{
public:
static void f(int ival_);
private:
inline static int i = 1;
const static int n = 1;
const static int m{2}; // C++11 起
const static int k;
constexpr static int arr[] = { 1, 2, 3 }; // OK C++11 起
// constexpr static int l; // 错误:constexpr static 要求初始化器
constexpr static int l{1}; // OK C++11 起
};
//test1.cpp
void Obj13::f(int ival_){};
const int Obj13::k = 3;
六、源码补充
编译指令g++ main.cpp test*.cpp -o test.exe -std=c++17(inline作用于成员变量)
main.cpp
#include "test1.h"
int main(int argc, char* argv[])
{
test1();
return 0;
}
test1.h
#ifndef _TEST_1_H_
#define _TEST_1_H_
class Commit_Access {
public:
int getreadonly() const{ return readonly; }
void setreadwrite(int value) { readwrite = value; }
int getreadwrite() const { return readwrite; }
void setwriteonly(int value) { writeonly = value; }
int getderivedataonly(){return derivedata;}
protected:
int derivedata; //子类可用
private:
int noaccess; // 纯内部数据,禁止访问这个 int
int readonly; // 可以只读这个 int
int readwrite; // 可以读/写这个 int
int writeonly; // 可以只写这个 int
};
class Derived : public Commit_Access
{
private:
/* data */
int indata;
public:
void setpdataonly( int val ){derivedata=val;} //
};
#include <ostream>
class PTest;
class FriendTest
{
private:
/* data */
public:
void doit();
void dosomething(PTest* const obj);
};
class PTest
{
private:
/* data */
int val;
public:
friend std::ostream& operator<<(std::ostream& os, const PTest& obj);
friend void FriendTest::doit();
friend void FriendTest::dosomething(PTest* const obj);
};
class A
{
private:
int val;
public:
};
class B{
private:
int val;
public:
};
class C
{
private:
static int si;
const char cc = 'D';
int ival;
double dval;
A a;
char *pc;
B *pb;
public:
};
class D
{
private:
bool b1;
char c1;
short s1;
int ival;
double dval;
int vec[5];
public:
};
/*
class D
{
private:
double dval;
bool b1;
int vec[5];
char c1;
int ival;
short s1;
public:
};
*/
#include <queue>
#include <mutex>
class CacheQue
{
private:
std::deque<std::string> msgque;
std::mutex msgmutex;
public:
//func
};
class ReadFromNet
{
private:
// std::deque<std::string> msgque;
// std::mutex msgmutex;
//other data
int readflag;
int ival;
//...
CacheQue msgs;
public:
//func
};
class Obj1
{
private:
/* data */
int id;
double val;
std::vector<int> vec; //OK
std::string str; //OK
public:
//default func
};
class Obj2
{
private:
/* data */
char* pstr;
public:
Obj2(){pstr = (char*)malloc(10*sizeof(char));}//手动显式构造函数
virtual ~Obj2(){delete[] pstr; pstr=nullptr;} //手动显式析构函数
//default func
};
class Obj3{ private: int ival;};
class Base1{
public:
Base1(){
ptr = new Obj3();
pc = new char[10];
};
virtual ~Base1(){
if(nullptr!=pc){delete[] pc; pc = nullptr;}
if(nullptr!=ptr){delete ptr; ptr = nullptr;}
};
private:
Obj3 *ptr;
char *pc;
};
class Obj4 : public Base1
{
public:
//...other
Obj4(): Base1(),val(0){ ptr = new Obj3(); }; //构造函数
~Obj4(){if(nullptr!=ptr){delete ptr; ptr = nullptr;}};// 析构函数
private:
const int a = 10; //
int val;
Obj3 *ptr; //
};
class Base2{
public:
Base2(int val){ ptr = new int(val);};
virtual ~Base2(){ if(nullptr!=ptr){delete ptr; ptr = nullptr;}};
private:
Base2(const Base2&) = delete;
Base2& operator=(const Base2&) = delete;
int *ptr;
};
class Obj5
{
public:
Obj5(const int &ival_, Base2* pc_) : icval(10),ival(ival_),pc(pc_){};
virtual ~Obj5(){ if(nullptr!=pc){delete pc; pc=nullptr;}};
private:
const int icval;//必须通过成员初始化列表进行初始化
int ival;
const Base2* pc;//必须通过成员初始化列表进行初始化
};
class Obj6 : public Base2
{
public:
Obj6(const bool& bflag_, const int& ival_, const double& dval_,const int& size=10)
: Base2(1),bflag(bflag_), ival(ival_), dval(dval_)/*, pc(nullptr)*/
{
pc = (char*)malloc(size*sizeof(char));
};
virtual ~Obj6(){ if(nullptr!=pc){delete pc; pc=nullptr;}};
private:
bool bflag;
int ival;
double dval;
char* pc;
};
class Obj7 : public Base2
{
public:
Obj7(const bool& bflag_, const int& ival_, const double& dval_,const int& size=10)
: ival(ival_), Base2(1), dval(dval_),bflag(bflag_)/*, pc(nullptr)*/
{
pc = (char*)malloc(size*sizeof(char));
};
virtual ~Obj7(){ if(nullptr!=pc){delete pc; pc=nullptr;}};
private:
bool bflag;
int ival;
double dval;
char* pc;
};
#include <vector>
#include <iostream>
class Obj8
{
public:
Obj8(int size_) :size(size_), ivec(size){};
~Obj8(){};
void test(){
std::cout << "size = "<<size<<"\n";
std::cout << "ivec.size = "<<ivec.size()<<"\n";
}
private:
int size;
std::vector<int> ivec;
};
#include <string>
class Obj9
{
public:
Obj9()= default;
Obj9(const Obj9&) = default;
Obj9& operator=(const Obj9&) = default;
private:
bool bval;
int ival;
std::string str;
};
// #include <ostream>
class MyString
{
public:
MyString(void); //默认构造函数
MyString( const char *str = nullptr ); //普通构造函数
MyString( const MyString &other ); //拷贝构造函数
~MyString( void ); //析构函数
MyString& operator=(const MyString &other); //赋值函数
MyString& operator=(const char* other); //赋值函数
operator char*() const; //MyString转char*
char* c_str(void) const; //取值(取值)
std::ostream& operator<<(std::ostream& output){
output << std::string(m_data);
return output;
};
friend std::ostream& operator<<(std::ostream& output,const MyString& obj){
output << std::string(obj.m_data);
return output;
};
private:
void init(const char *str);
void copy(const char *str);
private:
char *m_data;
};
class Obj10
{
public:
Obj10()= default;
private:
//方式一,自定义提供拷贝构造及复制赋值
// Obj10(const Obj10&){/*code*/};
// Obj10& operator=(const Obj10&){/*code*/return *this;};
//方式二,提供默认拷贝构造及复制赋值
// Obj10(const Obj10&) =default;
// Obj10& operator=(const Obj10&) = default;
//方式三,自定义提供空的拷贝构造及复制赋值
// Obj10(const Obj10&){/*不做任何处理*/};
// Obj10& operator=(const Obj10&){/*不做任何处理*/return *this;};
//方式四,强制删除拷贝构造及复制赋值,禁止任何函数调用
Obj10(const Obj10&) = delete;
Obj10& operator=(const Obj10&) = delete;
private:
bool bval;
int ival;
std::string str;
char* pc;
};
class Obj11
{
public:
Obj11(const int& ival_, const double& dval_=0.0)
: ival(ival_),dval(dval_){ };
Obj11(const Obj11& obj){
ival = obj.ival;
dval = obj.dval;
};
Obj11& operator=(const Obj11& obj){
if(this==&obj) return *this;
ival = obj.ival;
dval = obj.dval;
return *this;
};
private:
int ival;
double dval;
};
class X
{
public:
int getVal(void) const{return ival;}
void setVal(const int& ival_){
ival=ival_;
// ival_ += 1; //error
};
friend const X operator+(const X& lhs,const X& rhs){
X ret(lhs);
ret.ival+=rhs.ival;
return ret;
};
friend X operator-(const X& lhs,const X& rhs){
X ret(lhs);
ret.ival-=rhs.ival;
return ret;
};
private:
int ival;
};
class Obj12
{
public:
void func(){ std::cout<<"func()\n"; };
void func(const char& sval_){std::cout<<"func(const char&)\n"; };
void func(const int& ival_){std::cout<<"func(const int&)\n"; };
void g(const int& ival_=0){std::cout<<"g(const int&)\n";};
private:
int ival;
};
class Base3 {
public:
int doIt(){return 3;};// 一个叫做int doIt 的函数
};
class Base4
{
public:
void doIt(){};// 一个叫做void doIt 的函数
};
class Derived1: public Base3, public Base4 { public: };
class Derived2: public Base3{};
class Derived3: public Base3{};
class Derived4: public Derived2,public Derived3{};
class InlineTest
{
public:
void inl(int& ival_){}; //inline隐式
inline void f(int& ival_){}; //inline显式
void g(char& cval_);
constexpr void func(double& dval);//inline隐式,(C++11 起)
private:
inline static int n = 1; //inline变量
};
class Obj13
{
public:
static void f(int ival_);
private:
inline static int i = 1;
const static int n = 1;
const static int m{2}; // C++11 起
const static int k;
constexpr static int arr[] = { 1, 2, 3 }; // OK C++11 起
// constexpr static int l; // 错误:constexpr static 要求初始化器
constexpr static int l{1}; // OK C++11 起
};
void test1(void);
#endif //_TEST_1_H_
test1.cpp
#include "test1.h"
std::ostream& operator<<(std::ostream& os, const PTest& obj)
{
os << obj.val;
return os;
}
void FriendTest::doit()
{
PTest obj;
++obj.val;
};
void FriendTest::dosomething(PTest* const obj)
{
(obj->val)+=10;
};
#include <cstring>
//默认构造函数
MyString::MyString(void)
{
MyString(nullptr); //内部调用普通构造函数
}
//普通构造函数
MyString::MyString(const char *str)
{
init(str);
}
// MyString 的析构函数
MyString::~MyString(void)
{
delete [] m_data; // 或 delete m_data;
}
void MyString::init(const char *str)
{
if(nullptr==str)
{
m_data = new char[1]; //对空字符串自动申请存放结束标志'\0'的空
*m_data = '\0';
}else{
int length = strlen(str);
m_data = new char[length+1]; // 分配内存
strcpy(m_data, str);
}
};
void MyString::copy(const char *str)
{
if(nullptr!=str)
{
delete [] m_data; //释放原有的内存资源
int length = strlen( str );
m_data = new char[length+1]; //重新分配内存
strcpy( m_data, str);
}
};
//拷贝构造函数
MyString::MyString( const MyString &other ) //输入参数为const型
{
init(other.m_data);
}
//赋值函数
MyString &MyString::operator =( const MyString &other )//输入参数为const型
{
if(this == &other) //检查自赋值
return *this;
copy(other.m_data);
return *this; //返回本对象的引用
}
MyString& MyString::operator=(const char* other) //赋值函数
{
copy(other);
return *this; //返回本对象的引用
};
MyString::operator char*() const
{
return (char*)m_data;
};
char* MyString::c_str(void) const
{
return (char*)m_data;
};
#include <iostream>
#include <chrono>
const unsigned long sizel = 10000;
void initialization_test(void)
{
auto start = std::chrono::system_clock::now();
for (size_t row = 0; row < sizel/10; row++)
for (size_t i = 0; i < sizel; i++)
{
Obj6 obj6(i/2,i%2,i*1.0);
}
auto end = std::chrono::system_clock::now();
std::chrono::duration<double,std::milli> diff = end-start;
std::cout << "Obj6 diff.count() = " << diff.count() << "ms\n";
start = std::chrono::system_clock::now();
for (size_t row = 0; row < sizel/10; row++)
for (size_t i = 0; i < sizel; i++)
{
Obj7 obj7(i/2,i%2,i*1.0);
}
end = std::chrono::system_clock::now();
diff = end-start;
std::cout << "Obj7 diff.count() = " << diff.count() << "ms\n";
}
//
inline void InlineTest::g(char& cval_){}; //inline显式
constexpr void InlineTest::func(double& dval){};//inline隐式
//
void Obj13::f(int ival_){};
const int Obj13::k = 3;
void test1(void)
{
Obj5 obj5(1,new Base2(1));
//
// initialization_test();
//
Obj8 obj8(10);
obj8.test();
//
Obj9 Obj9_1;
Obj9 Obj9_2(Obj9_1);
Obj9 Obj9_3;
Obj9_2 = Obj9_1;
//
MyString mstr1("hello\n");
mstr1 << std::cout; //OK,不建议
std::cout << mstr1; //OK
char *pchar1 = mstr1; //operator char*() const
pchar1[0] = 'a';
pchar1 = nullptr;
std::cout << mstr1; //OK
char *pchar2 = mstr1.c_str();
// pchar2[0] = "b"; //error,不能将 "const char *" 类型的值分配到 "char" 类型的实体
std::cout << mstr1; //OK
MyString mstr2("");
mstr2 = mstr1;
MyString mstr3 = mstr2;
mstr3 = "world";
//
Obj11 Obj11_1(1), Obj11_2(2), Obj11_3(3), Obj11_4(4);
Obj11_1 = Obj11_2 = Obj11_3 = Obj11_4;
(Obj11_1 = Obj11_2) = Obj11_3 = Obj11_4;
Obj11_1 = (Obj11_2 = Obj11_3) = Obj11_4;
//
X x1,x2;
x1.setVal(10);
x2.setVal(20);
(x1-x2)=x1; //OK
// (x1+x2)=x1; //error
x1=x1+x2; //OK
//
Obj12 obj12;
obj12.func();
obj12.func('a');
obj12.func(0);
obj12.func(10);
obj12.g();
obj12.g('a');
obj12.g(0);
obj12.g(10);
//
Derived1 d1;
// d1.doIt(); // 错误!——二义
// int i1 = d1.doIt();// 错误!——二义
Derived4 d4;
// d4.doIt(); // 错误!——二义
};