类的基本属性
1. 类成员,成员函数,this指针
工作中我们通常需要用一个类来表示某一个对象例如,大数据项目中T-Box发送给CCU的数据是一串16进制的码流,这串码流的每一个字节都有自己的含义,但是我们需要对照文档,才能查询到每一个字节真实的意义,那我们工作时岂不是每次要用到这条消息的时候都要去查询文档,ccu不同模块的开发者也都要经常查询文档,这就很不方便,因此在我们收到消息时可以用一个类来表示这条消息,将码流的每个字节都解码成类的成员,这样只需要开发serviceinterface(即跟T-BOX对接的模块)人员必须清楚码流的含义,其他模块的人员则使用码流对应的类即可,以周期收集的Step1为例我们可以创建一个对应的类
class PeriodCollectReuqest
{
public:
PeriodCollectReuqest();
~PeriodCollectReuqest() override =default;
PeriodCollectReuqest(const PeriodCollectReuqest& other);
PeriodCollectReuqest& operator=(const PeriodCollectReuqest& other);
PeriodCollectReuqest(PeriodCollectReuqest&& other);
PeriodCollectReuqest& operator=(PeriodCollectReuqest&& other);
const uint8_t getNetwork() const { return network_; }
void setNetwork(const uint8_t& net) { network_ = net; }
const uint8_t getVersion() const { return version_; }
void setVersion(const uint8_t& version) { version_ = version; }
const uint16_t getUploadTime() const { return uploadTime_; }
void setUploadTime(const uint16_t& time) { uploadTime_ = time; }
const std::vector& getPeriodCollection() const { return periodCollections_; }
std::vector& movePeriodCollection() { return periodCollections_; }
private:
void copy(const PeriodCollectReuqest& other);
uint8_t network_;
uint8_t version_;
uint16_t uploadTime_;
std::vector periodCollections_;
};
那么PeriodCollectReuqest类中的变量即是PeriodCollectReuqest的成员,其中的函数即是成员函数。
我们使用访问说明符(access specifiers)来加强类的封装性。
定义在public说明符后的类成员可以在整个程序范围内被访问,public成员定义类的接口。
定义在private说明符后的类成员可以被类成员访问,private成员封装了类的实现细节。
(以上定义取自C++ Primer)
- 上例中PeriodCollectReuqest 类的成员均定义在private下,而对外我们则暴露getNetwork()等函数接口。这样子,PeriodCollectReuqest类以外的对象均只能拿到只读的PeriodCollectReuqest成员,而无法修改其成员。如果要修改则也只能通过我们提供的接口进行修改。
- this指针的本质是一个8个字节的指针,他的类型是该类的类型。我们可以通过this指针访问到类内部所有的成员和成员函数。我们平时在编码的过程中可以隐式的使用this指针,我们可以直接使用uploadTime_, 也可this->uploadTime_访问到uploadTime_。而在上例的成员函数中我们做了这样的定义 const uint16_t getUploadTime() const;这里的第一个const用来形容函数返回值是read only的,第二个const则是用来形容thsi指针的。因此getUploadTime()函数的代码块中只要是通过this指针引用的成员/成员函数都是只读的。
- 我们在设计类的时候应做到高内聚,低耦合。
要做到高内聚,我们要在private说明符后面多增加一些功能单一的function, function之间组合成一个class的各种实现细节。
要做到低耦合,我们的class对与其他class的依赖越少越好,当我们依赖的class接口发生变化时,对我们造成的影响应该只体现在依赖的接口上。
2. 类的构造、拷贝、赋值、析构、移动构造(c++11)、移动赋值(c++11)
如果我们在代码中用class关键字定义一个自定义类型PeriodCollectReuqest,PeriodCollectReuqest中没有一行额外的代码:
class PeriodCollectReuqest
{
};
那么c++编译器在编译时会为这个MyString生成6个函数即默认构造函数、拷贝构造函数、重载赋值运算符、析构函数、移动构造函数、重载移动赋值函数,如下:
class PeriodCollectReuqest
{
public:
PeriodCollectReuqest() = default;
PeriodCollectReuqest(const PeriodCollectReuqest&) = default;
PeriodCollectReuqest(PeriodCollectReuqest&&) = default;
PeriodCollectReuqest& operator=(const PeriodCollectReuqest&) = default;
PeriodCollectReuqest& operator=(PeriodCollectReuqest&&) = default;
~PeriodCollectReuqest() = default;
};
因此如果我们写了如下代码,编译器也可以顺利编译通过:
class PeriodCollectReuqest
{
};
int main()
{
PeriodCollectReuqest req1; // PeriodCollectReuqest();
PeriodCollectReuqest req2(req1); // PeriodCollectReuqest(const PeriodCollectReuqest&);
PeriodCollectReuqest req3(std::move(req2)); // PeriodCollectReuqest(PeriodCollectReuqest&&);
req1= req2; // PeriodCollectReuqest& operator=(const PeriodCollectReuqest&);
req1= std::move(req2); // PeriodCollectReuqest& operator=(PeriodCollectReuqest&&);
return 0;
}
3. 深拷贝和浅拷贝
这个概念在c++中并没有语法支持,而是我们在设计class时需要考虑的一个问题。如果你的class中没有指向堆区的类成员,那么我们在设计拷贝构造函数或者赋值重载函数时不考虑到深拷贝或者是浅拷贝都问题不大,他不会导致内存问题,但是如果你的class中有一个以上的指向堆区的类成员,那么我们在设计设计拷贝构造函数或者赋值重载函数时必须考虑到深拷贝或者是浅拷贝问题,否则可能会出现double free等不可定义的行为。
//一下代码实现深拷贝的拷贝构造函数
class MyString
{
public:
MyString(const char* input, int size) : itsStr(nullptr),itsSize(size)
{
itsStr = new char[itsSize];
std::copy(input, input+size, itsStr);
}
~MyString()
{
if(itsStr) { delete itsStr; }
itsStr = nullptr;
itsSize = 0;
}
MyString(const MyString& other)
{
itsSize = other.Size();
itsStr = new char[itsSize ];
std::copy(other.Str(), other.Str()+itsSize , itsStr);
}
const int& Size() const { return itsSize; }
void setSize(const int& size) const { itsSize = size; }
const char* Str() const { return itsStr; }
void setStr(const char* ptr) const { itsStr = ptr; }
private:
char* itsStr;
int itsSize;
};
//当然 这里强烈建议不要使用c++的原生指针,而使用智能指针
4. 移动构造
讲解移动构造之前我们需要了解一个c++11之后新增的特性: 右值和右值引用,一般的理解, “=”左边的值就是最值, "="右边的值就是右值,左值的生命周期取决与他所在的代码块,当他的代码块结束了,左值就会被系统释放掉。右值的生命周期更短,只是在使用时临时创建,使用后就归还给体统了。
int x = 6; // x是左值,6是右值
int& y = x; // 左值引用,y引用x
int& z1 = x * 6; // 错误,x*6是一个右值
const int& z2 = x * 6; // 正确,可以将一个const引用绑定到一个右值
int&& z3 = x * 6; // 正确,右值引用
int&& z4 = x; // 错误,x是一个左值
int fun()
{
return 10;
}
int&& num1 = fun(); //正确,函数返回值是右值引用
int& num2 = fun(); //错误,左值引用无法绑定右值
int num3 = fun(); //正确,此处num3得到的是fun返回值的拷贝
如果说左值引用相当于是一个对想象的标签,那右值引用可以理解为该对象内存的标签,右值引用传递时往往伴随内存的移动。
下面举个例子,假如我们现在有一个临时对象std::vector
struct LogSetParamReq
{
....
uint8_t logNum_;
uint16_t version_;
std::vector logBuffer_;
};
LogSetParamReq msg;
std::vector tmpVec = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
msg.logBuffer_ = tmpVec; //此时c++调用了std::vector的赋值重载函数vector& operator=( const vector& other );tmpVec 的内存完整的拷贝了一份到msg.logBuffer_
c++11之后提供了一个std::move函数,使用他可以有效的减少一些不需要的内存申请,我们可以这样使用std::move:
msg.logBuffer_ = std::move(tmpVec);
此后msg.logBuffer_拥有了tmpVec中的内存数据,而tmpVec中的数据被清空了,其过程可以这样理解:
在c++中std::move的实现其实就是将一个左值引用强转成右值应用,并返回出去:
template
decltype(auto) move(T&& param) //此处T是万能引用
{
using ReturnType = remove_reference_t&&;//remove_reference_t将入参引用的特性去除
return static_cast(param);
}
因此,在实际使用过程中,如果出现右值应用和std::move,那就往往暗示着此处需要做内存移动而非拷贝,结合MyString实现以下他的移动重载函数:
MyString(MyString&& other)
{
itsSize = other.Size();
itsStr = other.Str();
other.setSize(0);
other.setStr(nullptr);
return *this;
}
5. 使用 = default 和 = delete
当我们设计一个类的时候需要显示定义一个默认构造函数,但是里面什么都不用做,我们可能会写出这样的代码:
class Student
{
public:
Student() {}
}
c++11之后我们可以使用default关键字来定义一个默认构造函数,他比手动编写代码更有效
class Student
{
public:
Student() = default;
}
当我们要disable某些函数时,例如我们要禁止一个class被拷贝,使用传统c++我们只能将拷贝构造函数等卸载private下, 这样子外部程序就无法拷贝student:
class Student
{
private:
Student(const Student&) {}
}
而c++11之后新增了delete关键字来disable某些函数,代码如下:
class Student
{
public:
Student(const Student&) = delete;
}
6. 类的含参构造函数
上例中我们显示的定义了一个有参的构造函数,此时默认无参的构造函数不会自动生成,如果要用的话,则要自己显示的定义无参构造函数。
上例中MyString(const char input, int size)即是一个含参构造函数,它由函数名, 参数列表, 初始化列表,函数代码块组成,这里需要主义的是我们定义的class中所有的成员都应在初始化列表中显示的初始化。
7. explicit关键字
众所周知,c++在某些情况下,会为我们做一些隐式转换,我们最常见的隐式转换就是从c语言继承过来的基础类型的隐式转换,比如我们把一个uint16_t的变量赋值给一个uint8_t类型的变量,这么做编译完全没有问题,而在实际操作中uint16_t类型的变量的高8位被舍弃,低8位被赋值给目标变量。因此在实际操作中我们应尽量使用static_cast<>关键字显式的做类型转换,表明以下行为我都知道,其行为带来的后果我也知道(扯远了)。
uint16_t bigNumber = 100;
uint8_t number = bigNumber;
explicit关键字有什么用呢? 他是用来防止隐式转换的,例如,我们可以很容易的将一个c语言风格的字符串赋值给c++风格的std::string, 这里涉及到的操作就是隐式转换,在print()函数收到c语言风格字符串时,他会调用string(const char*)构造函数,构造一个std::string, 并传入到print()函数中, 参考以下代码:
void print(const std::string& str)
{
std::cout << str << std::endl;
}
print("hello world");//这里我们传了C语言风格的字符串,编译完全没问题
那么explicit关键字如何使用呢,其实也非常简单,只需要在构造函数前面加上即可,参考以下代码:
class MyString
{
public:
......
MyString(const char* input) : itsStr(nullptr), itsSize(12)
{
itsStr = new char[itsSize];
std::copy(input, input+12, itsStr);
}
......
private:
char* itsStr;
int itsSize;
};
void print(const MyString& str)
{
std::cout << str << std::endl;
}
print("hello world");
以上代码新增了一个只有一个参数的构造函数(且不论其实用性),我们同样可以直接嗲用print(const MyString& str)函数直接打印MyString, 此时发生的事情即使隐式转换,而如果我将代码修改如下:
explicit MyString(const char* input) : itsStr(nullptr), itsSize(12)
{
itsStr = new char[itsSize];
std::copy(input, input+12, itsStr);
}
此时会出现如下编译报错, 证明explicit work了:
error: invalid initialization of reference of type ‘const MyString&’ from expression of type ‘const char [12]’
print("hello world");
8. 运算符重载
我们都用过std::string对象,该对象除了存储了一个字符串外还提供了许多方便的接口,比如我们可以使用size()函数直接读取到std::string中字符串的长度,也可以使用 “+” 运算符,将两个字符串连接起来,那么他是如何做到的呢?参考以下代码:
我们将加数与被加数中的itsStr和itsSize成员结合起来,并构造出一个新的MyString对象返回出去。除此之外大部分运算符都是可以重载的,小部分比如sizeof()是不能被重载的。
class MyString
{
public:
MyString(const char* input, int size) : itsStr(nullptr), itsSize(size)
{
itsStr = new char[size];
std::copy(input, input+size, itsStr);
itsSize = size;
}
~MyString()
{
if(itsStr) { delete itsStr; }
itsStr = nullptr;
itsSize = 0;
}
MyString operator+(const MyString& other)
{
char* ch = new char[itsSize+other.Size()-1];
std::copy(itsStr, itsStr+itsSize-1, ch);
std::copy(other.Str(), other.Str()+other.Size(), ch+itsSize-1);
int size = itsSize+other.Size()-1;
return MyString(ch, size);
}
MyString(const MyString& other)
{
itsSize = other.Size();
itsStr = itsStr = new char[itsSize ];
std::copy(other.Str(), other.Str()+itsSize , itsStr);
}
const int& Size() const { return itsSize; }
const char* Str() const { return itsStr; }
private:
char* itsStr;
int itsSize;
};
int main(int argc, char* argv[])
{
MyString str1("hello", 6);
MyString str2(" world", 7);
MyString res = str1+str2;
std::cout << res.Str() << std::endl;
return 0;
}
9. 类的静态成员函数
有些时候我们要在类中定一个成员,他跟类本身直接相关,但跟类的其他成员内有任何关联。比如一个银行账户,里面有账户人的姓名,余额等都是这个账户本身的属性,当前的账户基础利率,他游离在账户之外,又与账户息息相关。此时我们可以定义一个静态(static)成员来表示账户基础利率。
类的静态成员定义必须要类的外部,而静态成员的初始化跟类没有任何关系,静态成员初始化参考普通静态变量是在程序加载时完成的。
静态成员函数使用时,也与类是否被初始化没有关系,可以直接使用。而如果在类的作用域外使用静态函数,则需要加类作用域加以说明。
class Account
{
public:
void calculate() { amount += amount * interestRate; }
static double rate() { return interestRate; }
static void setRate(const double& rate) { interestRate = rate; }
private:
std::string owner;
double amount;
static double interestRate;
};
double Account::interestRate {0.0};
int main()
{
Account::setRate(0.1);
}
10. class 和 struct的区别
它们之间只有一个区别,就是在默认状态下,class中的成员/成员函数都是private的,struct的成员/成员函数都是public的。struct也可以用有自己的成员,成员函数,静态成员,静态成员函数,this指针,各种构造函数,以及继承多态等属性。
11. 仿函数
再说仿函数之前,我们先看下面这个例子, 我们想要使用stl库中的std::sort函数对一个std::vector类型的数组进行排序,std::sort的第三个参数需要传一个函数指针/函数对象定义其排序的规则(从小到大,还是从大到小),我们可以定义一个函数,并将其函数指针传入到sort :
bool compare(const int& n1, const int& n2)
{
return n1 < n2; //从小到大
}
std::vector vec {1,4,3,2};
std::sort(vec.begin(), vec.end(), compare);
当然我们也可以借助class重载()运算符来定义一个仿函数来作为比较规则,传递给sort, 也可以得到同样的效果,代码参考如下:
class Compare
{
public:
bool operator()(const int& n1, const int& n2)
{
return n1 < n2;
}
};
std::vector vec {1,4,3,2};
std::sort(vec.begin(), vec.end(), Compare());