JAVA 转 C++ 必记 PartC
来源:互联网 发布:风景线自动打印软件 编辑:程序博客网 时间:2024/06/08 01:00
拷贝构造函数和拷贝赋值运算符
拷贝构造函数定义如下:
class User { string username; string phonenumbers[10];public: User(){ cout << "direct constructor !!! " << endl; }; User(string username) : username(username) { cout << "direct constructor (params: username ) !!!" << endl; }; User(const User& user):username(user.username) { int index = 0; for_each(begin(phonenumbers),end(phonenumbers),[&](string &p) mutable { p = user.phonenumbers[index++]; }); cout << "copy constructor !!! " << endl; }};
其中User(constUser& user):username(user.username) 是拷贝构造函数(copy constructor)的定义。这里需要注意的是copyconstructor 的参数一定是const 引用,原因是因为如果是普通的类型 进入函数会拷贝相关的类,而且相关的类的拷贝构造函数正是你调用的这个函数,会出现循环拷贝现象。同时需要注意的是:由于数组是无法拷贝的,所以我们应该将元素逐一抽出然后相应拷贝。
下面是关于什么情况会调用拷贝初始化
User userA = string("TONY"); //某些编译器是属于拷贝初始化,某些是跳过拷贝初始化,然后直接初始化。User userB = userA; //拷贝初始化User userC("YAN"); //直接初始化User userD(userC); //拷贝初始化
合成拷贝构造函数一般情况下,如果我们不去定义拷贝构造函数,系统会帮我们合成定义一个合成拷贝构造函数,合成拷贝构造函数(synthesizedcopy constructor)会将我们的非static 成员逐一拷贝,如果是数组也会将数组中的元素逐一拷贝(由于数组是不能拷贝的)
拷贝赋值运算符定义如下:
class User { string username; string phonenumbers[10];public:
.....中间省略1万字..... User& operator=(const User& user){ int index = 0; for_each(begin(phonenumbers),end(phonenumbers),[&](string &p) mutable { p = user.phonenumbers[index++]; }); this->username = user.username; cout << "copy-assignment operator !!! " << endl; };};
其中User&operator=(const User& user) 的定义就是拷贝赋值运算符(copy_assignment operator)的定义,函数体基本上和拷贝构造函数类似。
下面是什么情况会调用拷贝赋值运算符:
User userA = string("TONY"); //某些编译器是属于拷贝初始化,某些是跳过拷贝初始化,然后直接初始化。User userB = userA; //拷贝初始化userA = userB; //使用拷贝赋值运算符
合成拷贝赋值运算符(synthesizedcopy_assignment operator),一般情况下如我们没有定义相关的拷贝赋值运算符,会生成一个合成拷贝赋值运算符,而且会将我们的非static成员逐一拷贝,如果是数组也会将数组中的元素逐一拷贝(由于数组是不能拷贝的)
当然有些情况我们不希望合成一个拷贝构造函数或者运算符等,在新的标准下我们可以使用delete关键字定义一个函数为删除的,在旧的标准之下我们可以使用声明一个private的函数而不去定义,实现类似于delete的功能。
class MyObject {public : string text = "XXXXXX"; MyObject() = default; MyObject(const MyObject &) = delete; MyObject &operator=(const MyObject &) = delete; ~MyObject() = delete;};
由于一些类对象不希望使用拷贝的机制,例如IO流。
所以旧标准下我们可以通过声明一个private的拷贝构造函数去实现类似于delete的效果,但是由于友元和成员函数依然可以调用,所以我们也只做声明而不去定义:
class MyObject { MyObject(const MyObject &); //声明而不定义 MyObject &operator=(const MyObject &); ~MyObject();public : string text = "XXXXXX"; MyObject() = default;};
当成员函数和友元尝试调用的时候,会报错函数未定义,从而调用失败。
析构函数定义如下:
class User { string username; string phonenumbers[10];public: ~User(){ cout << "destructor !!!" << endl; };};
合成成员函数可能会被定义删除的可能有以下几种情况:
1、类中成员析构函数定义为删除或者是不可访问的(private)--- 合成析构函数定义为删除;
2、类中成员拷贝构造函数定义为删除的(或不可访问)、或者成员析构函数定义为删除(或不可访问)----合成拷贝构造函数定义为删除;
3、类中成员拷贝赋值运算符定义为删除(或不可访问)、或者类中有const或者引用成员---合成拷贝赋值运算符为删除;
4、类中成员析构函数定义为删除(或不可访问)、或者类中有引用或者const成员,且没有类内初始器----- 合成默认构造函数为删除;
在旧标准下定义为private从而禁止拷贝构造函数以及拷贝赋值运算符:
class TestObject { TestObject(const TestObject &); TestObject &operator=(const TestObject &);public: string text = ""; //类内初始化 TestObject() = default;};
需要注意的是:private定义在友元和成员函数当中仍然是可以使用的,所以我们只是进行声明而不定义,当友元和成员函数调用时会出错。
下面展示两个例子,一个是类似shared_ptr的类,一个是类似unique_ptr的类,其中类似unique_ptr的支持拷贝但是会进行值拷贝而非指针拷贝。由于还没有学习模板技术,所以指针中的数据是定义为string
MyValue_ptr:拷贝值互相独立,使用是堆内存所以在赋值过程会先释放之前的内存,然后重新获得新内存,而且还有一个数据修改以及访问计数器。
class MyValue_ptr {private: string *value; //数据值使用动态内存 int counter; //记录读取和修改次数public: MyValue_ptr(const string &value) : value(new string(value)), counter(0) {}; //拷贝构造函数 MyValue_ptr(const MyValue_ptr &myValue_ptr) : value(new string(*myValue_ptr.value)), counter(0) {}; //拷贝赋值运算符 MyValue_ptr&operator=(const MyValue_ptr& myValue_ptr){ string *new_value = new string(*myValue_ptr.value); if(this->value){ delete this->value; } this->value = new_value; this->counter = myValue_ptr.counter; return *this; } void swap(MyValue_ptr &ptrA,MyValue_ptr &ptrB){ using std::swap; swap(ptrA.value,ptrB.value); swap(ptrA.counter,ptrB.counter); } //重载赋值运算符 MyValue_ptr&operator=(const string& value){ if(this->value){ delete this->value; } this->value = new string(value); this->counter++; return *this; } //获得字符串 string& getValue(){ this->counter++; return *this->value; } //析构函数 ~MyValue_ptr(){ delete this->value; }};
MyShared_ptr:自建引用计数器和堆内存指针,在析构函数中需要检查其引用的数量,如果引用数量为零则释放内存。
class MyShared_ptr { string *value; int *reference_count;public: //构造函数,添加引用计数器并设置为1 MyShared_ptr(const string &value) : value(new string(value)),
reference_count(new int(1)) {}; //拷贝构造函数,并在引用计算器+1 MyShared_ptr(const MyShared_ptr &myShared_ptr) : value(myShared_ptr.value),
reference_count(myShared_ptr.reference_count) { (*this->reference_count)++; } //拷贝赋值运算符 MyShared_ptr &operator=(const MyShared_ptr &myShared_ptr) { auto new_value = myShared_ptr.value; auto new_reference_count = myShared_ptr.reference_count; (*new_reference_count)++; (*this->reference_count)--; if (this->reference_count != nullptr && *this->reference_count <= 0) { cout << "delete value " << *this->value << endl; delete this->value; delete this->reference_count; } this->reference_count = new_reference_count; this->value = new_value; return *this; } //赋值string只改变内容,不改变指针 MyShared_ptr &operator=(const string &value) { *this->value = value; return *this; } //获得数据等同于解引用 string &getValue() { return *this->value; } //对象析构时引用计数器-1 如果没有其他引用后销毁堆内存 ~MyShared_ptr() { (*reference_count)--; if (*reference_count <= 0) { cout << "delete value " << *this->value << endl; delete reference_count; delete value; } }};
有时我们可以利用swap进行对象转换,下面是关于MyValue_ptr类赋值运算符使用swap的写法:
MyValue_ptr&operator=(MyValue_ptr myValue_ptr){ swap(*this,myValue_ptr); return *this;}void swap(MyValue_ptr &ptrA,MyValue_ptr &ptrB){ using std::swap; swap(ptrA.value,ptrB.value); swap(ptrA.counter,ptrB.counter);}
提示:因为在传参过程中调用了拷贝构造函数,然后创建了一个函数中的myValue_ptr的局部变量,然后通过交换函数,将数据进行交换。然后myValue_ptr带着旧的数据在离开作用域后自动销毁并调用了myValue_ptr的析构函数。
根据上述的提示,MyShared_ptr也可以改成这样:
MyShared_ptr &operator=(MyShared_ptr myShared_ptr) { swap(*this, myShared_ptr); return *this;}void swap(MyShared_ptr &ptrA, MyShared_ptr &ptrB) { using std::swap; swap(ptrA.value, ptrB.value); swap(ptrA.reference_count, ptrB.reference_count);}
代码比MyShared_ptr&operator=(const MyShared_ptr& myShared_ptr) 简单了不少。
对象的移动
对象移动是新标准的一新特性,通过使用对象移动减少对象拷贝这种性能代价。但是需要注意,对象被移动后,需要确保不再使用原对象。
这里涉及到右值引用的概念:
右值引用是绑定到右值对象的引用使用&&来获得右值引用,右值引用最重要的特性--只能绑定在一个将要销毁的对象。因此我们可以使用右值引用的资源移动到一个对象当中。
关于右值和左值:这个概念原话是:一般而言,一个左值表达式的是一个对象的身份,而一个右值表达式表示的是对象的值。
一般读完都会有点懵逼,其实可以这样理解,左值是持久的,右值是短暂的。这样之间的区别就更加明显了,左值有持久状态,例如一个变量。而右值是字面量,或者是表达式求值过程中创建的临时对象。
下面几个就属于左值对象:
1、函数返回的临时对象
2、通过表达式计算后得出的值
3、字面量
在我们之前使用的所有左值对象引用只能通过const&的引用方式进行引用。以下是关于右值引用的例子:
int i = 42;int &r = i; //正确:左值引用,引用了一个左值对象int &&rr = i; //错误:无法将一个左值对象 引用绑定到右值引用int &r2 = i * 10; //错误:无法使用左值引用,绑定一个右值对象const int &r3 = i * 12; //正确:可以将一个右值对象绑定到const左值引用当中int &&rr2 = i * 13 // 正确:可以将一个右值对象绑定到右值引用当中。
int &&rr3 = i// 错误:变量是左值的。
右值引用只能绑定到临时对象当中,所以产生一下特点:
1、所引用的对象将要销毁
2、该对象没有其他用户
需要注意:通常我们使用const&左值引用绑定右值对象的时候,我们是不能对其进行修改的,但是如果我们使用&&右值引用是可以对其绑定进行修改的。
很多情况下我们需要将一个变量显性转换为右值:
int i=10;int &&rr = std::move(i); // 注意:这里使用的是std::move
需要注意的是:使用move函数之后我们只能销毁i或者对i这边变量从新赋值之外,我们不能做任何假设性操作。
对于类似string这种类型的拷贝是基本上产生了很多无谓的开销,string和其他内置类型都支持移动构造函数和移动赋值运算符:
string text = "TONY";string new_text = std::move(text);
可以做一个简单的测试,理解移动特性
text.push_back('A');cout << text << endl; //输出A 这里需要注意的是,一般情况下移动之后的原对象除了重新赋值和销毁就不做其他操作了cout << new_text << endl; //输出TONY
个人觉得是在底层移动了指针,从而达到移动的特性。其实就是复制其指针,由于在看C++11 primer 的例子中也是复制其指针的,以下我们使用MyShared_ptr定义一个移动构造函数:
//移动构造函数MyShared_ptr(MyShared_ptr &&myShared_ptr) : value(myShared_ptr.value), reference_count(myShared_ptr.reference_count) { myShared_ptr.value = nullptr; myShared_ptr.reference_count = nullptr;}
为了达到正常析构的目的,所以需要将析构函数改成:
//对象析构时引用计数器-1 如果没有其他引用后销毁堆内存~MyShared_ptr() { if (reference_count) { //判断计数器指针是否有效 (*reference_count)--; if (*reference_count <= 0) { cout << "delete value " << *this->value << endl; delete reference_count; delete value; } }}
当然如果是非指针类型的,就需要直接调用std::move调用其成员的移动构造函数。
下面是之前写的MyMessage和Folder的实验,添加其移动构造函数:
MyMessage::MyMessage(MyMessage &&message) : content(std::move(message.content)){ //移动string message.remove_self_in_folders(); this->folders = std::move(message.folders); //移动set this->save_self_in_folders(); message.folders.clear(); //确保其message对象能够正常析构}
[注:最后会贴出完成代码实现]
然而还有就是移动赋值运算符:
MyMessage& MyMessage::operator=(MyMessage &&message) { message.remove_self_in_folders(); this->remove_self_in_folders(); this->folders = std::move(message.folders); message.folders.clear(); this->save_self_in_folders();
return *this;}
当然一般情况下如我们不定义移动构造函数和移动赋值运算符都会有一个合成版本的,合成版本的会逐一移动成员,但是以下几点就导致合成定位为删除的:
1、类成员有自定义的拷贝构造函数或者类成员无法合成移动构造函数,将定义为删除
2、类成员的移动构造函数或者移动赋值运算符无法访问 --- 移动构造函数和移动赋值运算符都会定义为删除
3、如果类的析构函数被定义为删除 --- 移动构造函数和移动赋值运算符都会定义为删除
4、如果类成员有const 或者 引用 --- 移动构造函数和移动赋值运算符都会定义为删除
关于移动和拷贝的重载匹配
如果是右值对象有限匹配为移动,如果是左值对象匹配为const&。其实右值对象也可以匹配为const& 的拷贝函数,但是需要进行转型为cosnt,所以&&的移动函数匹配更加精准。
但是在实际测试上,在移动构造函数需要显示使用std::move才会成功调用,否则就是算右值对象也一样调用拷贝构造函数:
MyMessage messageA("Message_A");MyMessage messageB = messageA; //使用拷贝构造函数MyMessage &&rrMessage = getMyMessage(); //函数返回对象属于右值对象MyMessage messageC(getMyMessage()) ; //使用移动构造函数MyMessage messageD(std::move(getMyMessage())); //使用移动构造函数messageA = messageB; //传入左值使用拷贝运算符messageC = getMyMessage(); //赋值运算符,根据传入的是右值 所以使用移动赋值运算符
移动迭代器:
一般的迭代器解引用后都是右值的对象,当然我们可以显性使用std::move函数对对象进行转换,但是可以使用移动迭代解引用返回对象是右值,从而可以移动迭代器的对象。
下面是关于一个自定义的vector实验,将其中的扩容函数,修改成移动迭代器,从而使用移动的方式去将字符元素从旧的空间移动到新的空间当中,而不是去拷贝元素:
void StrVector::checkCap(int desired) { if (first_free + desired <= cap) { return; } auto cap_size = (first_free + desired) - elements; auto new_cap_size = cap_size * 2; string *new_memory_start = str_allocator.allocate(new_cap_size); string *new_first_free = uninitialized_copy(make_move_iterator(elements),
make_move_iterator(first_free),new_memory_start); this->free(); first_free = new_first_free; elements = new_memory_start; cap = elements + new_cap_size;}
以下是等价方式:
void StrVector::checkCap(int desired) { if (first_free + desired <= cap) { return; } auto cap_size = (first_free + desired) - elements; auto new_cap_size = cap_size * 2; string *new_memory_start = str_allocator.allocate(new_cap_size); auto new_memory_iterator = new_memory_start; for (auto item = elements; item != first_free; item++) { str_allocator.construct(new_memory_iterator++, std::move(*item)); } this->free(); first_free = new_memory_iterator; elements = new_memory_start; cap = elements + new_cap_size;}
关于类的右值成员函数
一般情况下我们没有区分调用函数的是右值还是左值,所以以下表达式是成立的:
string a = "tony";string b = "yan";auto n = (a + b).find('a'); //使用右值调用成员函数a + b = "hahaha"; //为一个右值对象赋值
对于上述傻B行为可以得出一个结论,上面都是然并卵的。所以有时我们需要对这些行为加以阻止,或者对右值的调用只是做一些有意义的操作。
class RightValueTest { string value;public: RightValueTest(string value) : value(value) {}; //调用的对象是一个非const的左值对象 RightValueTest &operator=(const RightValueTest &value) & { this->value = value.value; return *this; }};
在函数后面加上&符号代表接受左值对象调用,如果是右值调用会出错:
int main() { RightValueTest testC("TEST_C"); getRightValueTest() = testC;}
错误如下:
C:\Users\TONY\ClionProjects\startLearing\main.cpp:15:25:error: passing 'RightValueTest' as 'this' argument discards qualifiers[-fpermissive]
getRightValueTest() = testC;
同样来说右值函数也可以和左值重载,但是需要注意的是不能两个都非const其中一个函数必须是const否则无法重载,以下是重载的方式:
class RightValueTest { string value;public: RightValueTest(string value) : value(value) {}; char getValueChar(int index) const &{ cout << "left value" << endl; return *(this->value.begin() + index); } char getValueChar(int index) &&{ cout << "right value" << endl; char tmp = *(this->value.begin() + index); return std::move(tmp); }};
和constthis的重载一样,考究的是调用的对象是一个const左值 还是一个 右值对象,下面是调用的结果:
RightValueTest getRightValueTest() { RightValueTest testA("TEST_A"); return testA;}int main() { const RightValueTest testB("TEST_C"); char resultA = getRightValueTest().getValueChar(5); //使用右值版本 char resultB = testB.getValueChar(5); //使用const左值版本}
自定义StrVector代码如下:
StrVector.h
#ifndef UNTITLED_STRVECTOR_H#define UNTITLED_STRVECTOR_H#include <string>#include <initializer_list>using namespace std;class StrVector { string *elements; string *first_free; string *cap; allocator<string> str_allocator; void checkCap(int desired); void free();public: StrVector(); StrVector(initializer_list<string> initializerList); StrVector(const StrVector& strVector); StrVector& operator=(const StrVector& strVector); string* push_back(string value); string* push_back(initializer_list<string> initializerList); int size(); string* begin(); string* end(); string* begin() const; string* end() const; ~StrVector(); void clear(); void show();};#endif //UNTITLED_STRVECTOR_H
StrVector.cpp
StrVector::StrVector() { elements = str_allocator.allocate(5); first_free = elements; cap = elements + 5;}StrVector::StrVector(const StrVector &strVector) { elements = str_allocator.allocate(strVector.cap - strVector.elements); first_free = uninitialized_copy(strVector.begin(), strVector.end(), elements); cap = elements + (strVector.cap - strVector.elements);}StrVector &StrVector::operator=(const StrVector &strVector) { clear(); checkCap(strVector.cap - strVector.elements); first_free = uninitialized_copy(strVector.begin(), strVector.end(), elements); return *this;}StrVector::StrVector(initializer_list<string> initializerList) { elements = str_allocator.allocate(initializerList.size() * 2); cap = elements + initializerList.size() * 2; first_free = uninitialized_copy(initializerList.begin(), initializerList.end(), elements);}string *StrVector::begin() { return this->elements;}string *StrVector::end() { return this->first_free;}string *StrVector::begin() const { return this->elements;}string *StrVector::end() const { return this->first_free;}void StrVector::checkCap(int desired) { if (first_free + desired <= cap) { return; } auto cap_size = (first_free + desired) - elements; auto new_cap_size = cap_size * 2; string *new_memory_start = str_allocator.allocate(new_cap_size);// auto new_memory_iterator = new_memory_start;// for (auto item = elements; item != first_free; item++) {// str_allocator.construct(new_memory_iterator++, std::move(*item));// } string *new_first_free = uninitialized_copy(make_move_iterator(elements), make_move_iterator(first_free), new_memory_start); this->free();// first_free = new_memory_iterator; first_free = new_first_free; elements = new_memory_start; cap = elements + new_cap_size;}string *StrVector::push_back(string value) { checkCap(1); str_allocator.construct(first_free, value); return ++first_free;}string *StrVector::push_back(initializer_list<string> initializerList) { checkCap(initializerList.size()); first_free = uninitialized_copy(initializerList.begin(), initializerList.end(), first_free); return first_free;}int StrVector::size() { return first_free - elements;}StrVector::~StrVector() { this->free();}void StrVector::clear() { for (auto item = elements; item != first_free; item++) { str_allocator.destroy(item); } first_free = elements;}void StrVector::free() { this->clear(); str_allocator.deallocate(elements, cap - elements);}void StrVector::show() { for_each(begin(), end(), [](string &item) { cout << item << " " << flush; }); cout << endl;}
MessageAndFolder代码如下:
MyMessage.h
#ifndef UNTITLED_MESSAGE_H#define UNTITLED_MESSAGE_H#include <string>#include <set>using std::string;using std::set;class Folder;class MyMessage { string content; set<Folder*> folders; void delete_self(); void remove_self_in_folders(); void save_self_in_folders();public: MyMessage(string content); MyMessage(MyMessage &message); MyMessage(MyMessage &&message) noexcept; MyMessage &operator=(const MyMessage &message); MyMessage &operator=(MyMessage &&message); void swap(MyMessage &ma, MyMessage &mb); MyMessage &save(Folder &folder); MyMessage &remove(Folder &folder); void showFolder(); ~MyMessage(); void remove_from_folderset(Folder &folder); void save_into_folderset(Folder &folder); string getMessageContent();};#endif //UNTITLED_MESSAGE_H
MyMessage.cpp
#include <iostream>#include "MyMessage.h"#include "Folder.h"void MyMessage::swap(MyMessage &ma, MyMessage &mb) { using std::swap; ma.remove_self_in_folders(); mb.remove_self_in_folders(); swap(ma.content, ma.content); swap(ma.folders, ma.folders); ma.save_self_in_folders(); mb.save_self_in_folders();}void MyMessage::delete_self() { remove_self_in_folders(); content = "";}MyMessage &MyMessage::operator=(const MyMessage &message) { cout << "copy operator : " << content << endl; remove_self_in_folders(); this->folders = message.folders; save_self_in_folders(); return *this;}MyMessage &MyMessage::remove(Folder &folder) { folder.delete_message(*this); this->folders.erase(&folder); return *this;}MyMessage &MyMessage::save(Folder &folder) { folder.save_mesage(*this); this->folders.insert(&folder); return *this;}MyMessage::MyMessage(string content) : content("normal-" + content) { cout << "normal constructor : " << this->content << endl;}MyMessage::MyMessage(MyMessage &message): content(message.content), folders(message.folders) { save_self_in_folders(); cout << "copy constructor : " << content << endl;}MyMessage::MyMessage(MyMessage &&message) noexcept : content(std::move(message.content)){ //移动string cout << "move constructor : " << content << endl; message.remove_self_in_folders(); this->folders = std::move(message.folders); //移动set this->save_self_in_folders(); message.folders.clear();}MyMessage& MyMessage::operator=(MyMessage &&message) { cout << "move operator : " << content << endl; message.remove_self_in_folders(); this->remove_self_in_folders(); this->folders = std::move(message.folders); message.folders.clear(); this->save_self_in_folders(); return *this;}MyMessage::~MyMessage() { delete_self();}void MyMessage::showFolder() { for (set<Folder*>::iterator folder_iter = this->folders.begin(); folder_iter != this->folders.end(); folder_iter++) { cout << (*folder_iter)->getFolderName() << " " << flush; } cout << endl;}void MyMessage::remove_self_in_folders() { for (set<Folder*>::iterator folder_iter = this->folders.begin(); folder_iter != this->folders.end(); folder_iter++) { (*folder_iter)->delete_message(*this); } this->folders.clear();}void MyMessage::save_self_in_folders() { for (set<Folder*>::iterator folder_iter = this->folders.begin(); folder_iter != this->folders.end(); folder_iter++) { (*folder_iter)->save_mesage(*this); }}string MyMessage::getMessageContent() { return this->content;}void MyMessage::save_into_folderset(Folder &folder) { this->folders.insert(&folder);}void MyMessage::remove_from_folderset(Folder &folder) { this->folders.erase(&folder);
Folder.h
#ifndef UNTITLED_FOLDER_H#define UNTITLED_FOLDER_H#include <string>#include <set>class MyMessage;using namespace std;class Folder { string folder_name; set<MyMessage*> messages; void delete_self(); void remove_from_messages(); void save_into_messages();public: string getFolderName(); Folder(string folder_name) : folder_name(folder_name) {}; Folder(const Folder &folder); Folder &operator=(const Folder &folder); Folder &save(MyMessage &message); Folder &remove(MyMessage &message); void swap(Folder& fa,Folder& fb); void showMessages(); void delete_message(MyMessage& message); void save_mesage(MyMessage& message); ~Folder();};#endif //UNTITLED_FOLDER_H
Folder.cpp
#include <algorithm>#include <iostream>#include "Folder.h"#include "MyMessage.h"void Folder::delete_message(MyMessage &message) { this->messages.erase(&message);}Folder::Folder(const Folder &folder) : folder_name("copy_" + folder.folder_name), messages(folder.messages) { save_into_messages();}void Folder::delete_self() { remove_from_messages(); this->folder_name = "";}string Folder::getFolderName() { return this->folder_name;}Folder &Folder::operator=(const Folder &folder) { this->folder_name = "copy_" + folder.folder_name; remove_from_messages(); this->messages = folder.messages; save_into_messages(); return *this;}Folder &Folder::remove(MyMessage &message) { message.remove_from_folderset(*this); this->messages.erase(&message); return *this;}Folder &Folder::save(MyMessage &message) { message.save_into_folderset(*this); this->messages.insert(&message); return *this;}void Folder::save_mesage(MyMessage &message) { this->messages.insert(&message);}void Folder::showMessages() { for_each(this->messages.begin(), this->messages.end(), [](MyMessage *message) { cout << message->getMessageContent() << " " << flush; }); cout << endl;}void Folder::swap(Folder &fa, Folder &fb) { using std::swap; fa.remove_from_messages(); fb.remove_from_messages(); swap(fa.folder_name,fb.folder_name); swap(fa.messages,fb.messages); fb.save_into_messages(); fa.save_into_messages();}Folder::~Folder() { delete_self();}void Folder::save_into_messages() { for (set<MyMessage*>::iterator item = this->messages.begin(); item != this->messages.end(); item++) { (*item)->save_into_folderset(*this); }}void Folder::remove_from_messages() { for (set<MyMessage*>::iterator item = this->messages.begin(); item != this->messages.end(); item++) { (*item)->remove_from_folderset(*this); }}
重载运算符
不能被重载的运算符有:
::(作用域运算符) .* . ? :(三目运算符)
以下运算符不建议重载:
||(逻辑或) &&(逻辑与) ,(逗号)都不建议重载,因为他们的重载的运算符不会保留其运算对象的求值顺序,而且也无法保留其内置运算符的短路求值属性,所以我们一般都不会去重载这些运算符。
运算符函数可以定义为类成员函数也可以定义成非类成员函数,当然有些运算符应该定义成为成员函数成为合理其他则反之。但是有些运算符重载也规定了一定必须是成员函数或规定了一定是非成员函数。
1、赋值( = )、下标( [] )、成员访问运算符(->),都必须要是成员函数;
2、复合预算符(+=、-=等)建议是成员函数;
3、会更改对象其状态的运算符(++,--,*等),建议是成员函数;
4、具有对称性的运算符可以转换任何一端的运算对象的运算符(+、-、/等)建议是非成员函数;
5、输入输出运算符必须是非成员函数;
以下是一个自定义string,定义对称性运算符+号,在成员函数当中,会出现以下情况:
class MyString {private: string value;public: friend ostream &operator<<(ostream &os, const MyString &string); MyString(const string &value) : value(value) {}; MyString(const char *str_p = nullptr) : value(string(str_p)) {}; MyString operator+(const MyString &rightValue){ string new_value = this->value + rightValue.value; return MyString(new_value); }};ostream &operator<<(ostream &os, const MyString &string);
这样调用就会出现以下问题:
MyString mystringA = "TONYB";MyString mystringB = " YAN";//合法,因为Mystring的构造函数可以通过string 或者 char指针隐性转换MyString mystring1 = mystringA + " HAHAHAHA";//错误,因为定义成为成员函数,左侧运算对象必须是MyString对象MyString mystring2 = " AAA" + mystringA;//合法左右则运算符直接就是MyString对象无需转型cout << mystring1 + mystringB << endl;
如果我们将对称运算符定义为非成员函数,唯一要求就是左则和右则运算对象必须要有一方是类对象。
class MyString {private: string value;public: friend ostream &operator<<(ostream &os, const MyString &string); MyString(const string &value) : value(value) {}; MyString(const char *str_p = nullptr) : value(string(str_p)) {};};ostream &operator<<(ostream &os, const MyString &string);//注意在头文件定义运算符对象必须使用inline否则会出现重复定义现象inline MyString operator+(const MyString &leftValue,const MyString &rightValue){ string new_value = leftValue.value + rightValue.value; return MyString(new_value);}
所以以下的调用可以变成这样
//合法,因为Mystring的构造函数可以通过string 或者 char指针隐性转换MyString mystring1 = mystringA + " HAHAHAHA";//合法,定义成为非成员函数,左侧和右则运算对象都可以转换成目标类型MyString mystring2 = " AAA" + mystringA;//不合法,左则或者右则必须其中一方是类类型对象MyString mystring1 = "AAA" + "AAA";
输入输出运算符:
输入输出运算符必须是非成员函数;
以下是输出运算符的重载
注意:如果是在头文件定义运算符重载必须是inline
class MyString {private: string value;public: friend ostream &operator<<(ostream &os, const MyString &string); MyString(const string &value) : value(value) {}; MyString(const char *str_p = nullptr) : value(string(str_p)) {};};ostream &operator<<(ostream &os, const MyString &string);
定义如下:
ostream& operator<<(ostream &os,const MyString &string) { os << string.value; return os;}
输出运算符返回的是output流的引用,当然左侧运算符一定也是output流运算符的,然后右则运算符是相应的对象,同样因为IO流不能拷贝而且IO流对象在使用过程当中会修改其状态所以不能定义为const。
下面是定义>>重载运算符
istream &operator>>(istream &inputs, MyString &str) { inputs>> str.value; return inputs;}
注意如果流操作失败需要将其数据重置,以免出现问题其他业务问题。如果执行输入当中出现应该将failbit置位提示调用者在输入的时候出现问题,并进行相关处理工作。
istream &operator>>(istream &inputs, MyString &str) { inputs >> str.value; if(str.value == "fuck"){ inputs.setstate(inputs.failbit); } return inputs;}
需要注意的是,输入运算符不要将右侧运算符设置为const,否则就会死循环。
重载比较运算符
inline bool operator==(const MyString &strA, const MyString &strB) { return strA.value == strB.value;}inline bool operator!=(const MyString &strA, const MyString &strB) { return !(strA == strB);}inline bool operator<(const MyString &strA, const MyString &strB) { return strA.value < strB.value;}inline bool operator>(const MyString &strA, const MyString &strB) { return strA.value > strB.value;}
一理通百理明~不再介绍了。
赋值运算符:
赋值运算符和复合运算符都需要时成员函数
赋值运算符之前已经使用过,再贴出一下相关的代码:
MyString& operator=(const MyString &myString){ this->value = myString.value; return *this;}
同理复合运算符也应该定义在成员函数当中
MyString& operator+=(const MyString &myString){ this->value+=myString.value; return *this;}
下标运算符
下标运算符因为有可能在const和非const的对象中获得其中元素的值,所以最好同时定义const和非const的版本,而且下标运算符应该定义为类的成员函数,下面使用之前自定义的StrVector定义下标运算符,并尝试使用。
定义如下:
string& operator[](std::size_t index);const string& operator[](std::size_t index) const;
实现如下:
string& StrVector::operator[](std::size_t index) { auto str_p = this->begin() + index; return *str_p;}const string& StrVector::operator[](std::size_t index) const { auto str_p = this->begin() + index; return *str_p;}
调用基本上与使用其他容器一致:
StrVector strVector = {"TONYA","TONYB","TONYC","TONYD","TONYE"};strVector.show();strVector[2] = "YAN";strVector.show();cout << strVector[3] << endl;
递增递减运算符(--、++):
由于递增递减运算符比较坑爹,有前置运算符和后置运算符之分。所以在重载的时候要比较小心,而且递增递减运算符必须是成员函数。
为了便于介绍递增递减运算符我会使用自定义的MyNumber类去展示。
class MyNumber { int number;public: friend ostream& operator<<(ostream& os,const MyNumber& nb); MyNumber(const int n) : number(n) {}; MyNumber& operator++(){ ++(this->number); return *this; } //后置运算符,由于要有别于前置运算符,所以定义需要添加一个只用于占位的形参数 MyNumber operator++(int n){ MyNumber number_tmp = *this; ++(this->number); return number_tmp; }};inline ostream& operator<<(ostream& os,const MyNumber& nb){ return (os << nb.number);}
调用如下:
MyNumber myNumber = 10;//后置运算符cout << myNumber++ << endl; //显示10//前置运算符cout << ++ myNumber << endl; //显示12
后置运算符,由于要有别于前置运算符,所以定义需要添加一个只用于占位的形参数,但是在调用过程之中就好自定选择好相应的重载版本。
成员访问运算符(*、->)
我们通过MyShared_ptr进行演示解引用的运算符重载:
//解引用运算符string &operator*() { return *(this->value);}//重载解引用string *operator->() { return this->value;}
其中*非常好理解,就直接返回一个当前指向的string的引用即可,便于进行修改以及其他操作,如果定义的话最好也定义const版本和非const的版本的*运算符重载。
但是->就不太好理解了,因为其返回的是一个指针,这是因为->有点特别,在调用过程之中他会一直迭代循环的去调用其对象之中的->。所以我们可以这样去理解这个运算符定义的意义:
MyShared_ptr myShared_ptr = "TONY";cout<< myShared_ptr->size() << endl;
上述的myShare_ptr->size()可以解析为 调用了myShared_ptr->的运算符重载,返回了一个指针然后指针循环迭代调用其指针的底层->运算符函数定义 严格来说就是:myShared_ptr->(返回一个*string)->size()
至于*的调用就和往常调用一致了:
cout<< *myShared_ptr << endl;
函数调用运算符:
函数运算符其实就是定义()即调用函数的运算符,这里我们举个简单的列子(定义了函数运算符的类的对象称为---函数对象,lambda表达式也属于函数对象):
class Sum {public: int operator()(const int &a,const int &b){ return a + b; }};
定义了一个函数调用运算符重载函数,我们在使用的方式如下:
Sum sum;cout << sum(10,10) << endl;
这样与函数指针类似也和lambda表达式通过变量获得后调用相似。
同样我们都可以定义成员属性,去记录状态:
class Sum { int start; Sum(int s=0) : start(s) {};public: int operator()(const int &a, const int &b) { return start + a + b; }};
同样地我们可以利用函数对象传入需要谓词的函数当中:
class ForCheck { ostream &os;public: ForCheck(ostream &os) : os(os) {}; void operator()(string &item) { os << item << " " << flush; }};
定义ForCheck类然后生成函数对象,并传入for_earch循环当中:
ForCheck check(cout);StrVector myVec = {"TONYA","TONYB","TONYC"};for_each(myVec.begin(),myVec.end(),check);
Lambda表达式与函数对象的关系:
其中Lambda表达式是编译器生成的一个匿名类的函数对象,其中捕获的值就等于是该类的成员数据(属性),其命名与捕获的值名称一致,但是如果不定义成为mutable其lambda表达式的成员数据都为cosnt。
同样标准库当中都定义了函数对象
算术
关系
逻辑
plus<Type>
equal_to<Type>
logical_and<Type>
minus<Type>
not_equal_to<Type>
logical_or<Type>
multiplies<Type>
greater<Type>
logical_not<Type>
divides<Type>
greater_equal<Type>
modulus<Type>
less<Type>
negate<Type>
less_equal<Type>
下面是使用标准库函数对象的好处:
vector<int *> int_vec;int na = 95;int nb = 52;int nc = 156;int_vec.push_back(&na);int_vec.push_back(&nb);int_vec.push_back(&nc);sort(int_vec.begin(),int_vec.end());
上述列子当中排序是按照其指针进行排序,但是我们可以利用less函数对象进行排序,函数对象会取出其值进行比较。
sort(int_vec.begin(),int_vec.end(),less<int *>());
提示:关联容器的关键字默认使用less函数对象进行比较排序,所以不需要担心其关键字的排序问题。
可调用对象与function
C++当中有几种可调用的对象:函数、函数指针、lambda表达式、bind创建对象、重载函数调用运算符的类对象,虽然他们的类型都不一致,但是却可以共享同一种调用形式:
Int(int,int) 是一个函数类型也是一种调用形式(call signature)
不同的类型可以具有相同的调用形式:
int add(int a, int b) { return a + b;}int main() { auto mod = [](int a, int b) { return a % b; }; struct divide { int operator()(int a, int b) { return a / b; }; };}
三者其调用形式一致,所以我们可以希望通过一个map或者set相同调用形式的对象进行存储,但是如果我们在map和set中定义模板为函数指针的模板这样mod和divide这样的可调用对象就无法存储,但是在实际过程中发现lambda表达式的确能够存储在map当中
divide new_divide;map<string,int(*)(int,int)> binops;binops.insert({"+",add}); //合法add是一个函数可以准备为函数指针。binops.insert({"%",mod}); //lambda表达式对象可以转换为函数指针,但是C++ primer描述不能转换。binops.insert({"/",new_divide}); //不合法,不能将对象转为函数指针。
如果我们希望将同一个调用形式的对象或者是函数指针都可以共同存储的话,我们可以使用function类。
以下是关于function类的用法:
function<int(int,int)> function_mod = mod;function<int(int,int)> function_divide = new_divide;function<int(int,int)> function_add = add;cout << function_mod(10,10) << endl;cout << function_add(10,10) << endl;cout << function_divide(10,2) << endl;map<string,function<int(int,int)>> binops;binops.insert({"/",function_divide});binops.insert({"%",function_mod});binops.insert({"+",function_add});binops.insert({"/",new_divide}); //同时也支持隐性转换
但是需要注意的是,function类不能支持函数重载传入:
int add(int a, int b) { return a + b;}double add(double a, double b) { return a + b;}int main() { //错误,未知那个add函数 function<int(int,int)> functionAdd = add;}
所以我们需要使用函数指针的方式进行传入已经重载的函数:
int add(int a, int b) { return a + b;}double add(double a, double b) { return a + b;}int main() { int(*int_add)(int,int) = add; function<int(int,int)> functionAdd = int_add; cout << functionAdd(10,10) << endl;}
类型转换也有运算符,类型转换运算符:
我们将一个对象转换成其他类型的对象,可以使用类型转换运算符重载的方式去实现:
class MyNumber { int number;public: MyNumber(const int n) : number(n) {}; operator int() const{ return this->number; }};
需要注意一个准则:类型转换运算符既没有显式的返回类型,也没有形参,而且必须定义类的成员函数。类型转换运算符通常不应该改变对象的内容,因此,类型转换运算符一般定义为const成员。
MyNumber numberA = 12; //使用构造函数转换int a = numberA; //使用类型转换运算符转换
OOP 面向对象
C++ 的OOP基本和JAVA右非常多的相似地方,有抽象类、派生类、直接基类、间接基类、抽象函数等。下面会抽选一点和JAVA不一样的方法进行介绍
Protected访问控制符出现
和JAVA一样基累的protected可以给自己的子类进行使用,使用方式和之前的类定义访问控制符一样:
class Product {public: string product_name; double price;protected: int quantity;};
子类可以访问protected除此之外不能访问
class Food : public Product {public: int get_quantity(){ return this->quantity; }};
上面可以看到继承是通过:运算符 然后外加一个访问控制符定义直接基类的。
下面将会讲述继承当中的访问控制符作用:
class Vegetable : public Food {public: int get_quantity(){ return this->quantity; }};
可以看到Vegetable类继承Food类,是可以获得Food类从Product类继承而来的quantity成员变量,我们可以在Food类使用private访问控制符去继承Product就让Vegetable对quantity不可访问了。
class Food : private Product {
其访问控制符最终作用是将继承而来的所有基类成员变成private,public,protected。所以如我们定义了private在类的对象使用之中连继承而来的public属性特无法使用,同时Food的派生类也无法访问Product这个间接基类的成员属性
int main() { Food food; food.product_name; //错误无法访问}
Food的派生类
class Vegetable : public Food {public: int get_quantity(){ return this->quantity; //错误无法访问 }};
同理如果我们把Food继承Product的访问控制符改成protected所有继承得到的成员在其派生类是可见的。
跟JAVA一样,类具有多态性,但是其多态性必须通过虚函数去实现,虚函数是一个希望其派生类进行重写的类,同时实现也是实现多态性的重要途径:
class Product {public: ........ virtual void show_product(){ cout << "product : " << product_name << endl; } ........};
Food对show_product进行覆写:
class Food : private Product {public: ......... virtual void show_product(){ cout << "Food Prodcut name : " << this->product_name << endl; }};
注意的是,我们只能使用指针和引用实现类的多态性:
shared_ptr<Product> product = make_shared<Product>();product->show_product();product = make_shared<Food>();product->show_product();
注意对象是不能实现多态性的
Product product;Food food;product = food; //合法,但是只是这里只是使用了合成拷贝赋值运算符函数,因为这里转换成为了const Product& product,所以只是拷贝了相应的Product类部分的数据。product.show_product(); //还是使用Product的虚函数
如果我们不去实现自己的虚函数将会直接使用其基类的虚函数:
class Vegetable : public Food { };
Vegetable类是一个空类,我们使用它的对象调用show_product函数是调用其基类的虚函数:
shared_ptr<Product> product = make_shared<Product>();product = make_shared<Vegetable>();product->show_product();
派生类构造函数:
派生类构造函数需要使用其基类的构造函数:
class Product {public: string product_name; double price; Product(string productName, double price) : product_name(productName), price(price) {}; ..........protected: int quantity;};
Food类:
class Food : public Product {private: string expiration_date;public: Food(const string &productName, double price, string expiration_date) : Product(productName, price), expiration_date(expiration_date) {} ..........};
使用基类构造函数进行初始化派生类的基类部分。
Static静态成员的继承
class Product {public: static string company_name ;
};
使用方式也JAVA基本一致:
Product product("NEW_PRODUCT",100);product.company_name;Food food("NEW_PRODUCT2",100,"2017-5-10");food.company_name;Product::company_name;Food::company_name;
上述调用均合法
阻止继承
使用final关键字可以阻止类被继承:
class Product final {
静态类型和动态类型
所谓的静态类型即时对象本身的类型,动态对象即时其指向的真实类型。
虚函数的形参
需要注意的是,基类虚函数的形参必须和虚函数的形参一致,否则就不算是覆盖则是另外一个不一样的虚函数,也没有办法实现多态性,以免出现这种问题,我们可以使用override说明符,说明这个函数是重写基类的虚函数,编译器检查此函数不是重写基类的虚函数则马上报错,从而发现错误:
class Food : public Product { ......... virtual void show_product() override { cout << "Food Prodcut name : " << this->product_name << endl; }};
由于派生类的虚函数重写,可以不加上virtual关键字的。编译器会默认为派生类重写的虚函数加上virtual,所以如果我们不希望再下一层的派生类重写其虚函数的话我们可以使用final关键字加以阻止:
class Food : public Product { ......... void show_product() override final { //不加virtual关键字仍然合法 cout << "Food Prodcut name : " << this->product_name << endl; }};
需要以下没有报错,但是Vegetable类的show_product是完全不一样的函数,所以不具备多态性,我们加上override验证就会出错。
class Vegetable : public Food {public: Vegetable(const string &productName, double price, const string &expiration_date); void show_product(){ }};
再使用多态性调用过程中也会出现错误,因为final的函数阻止了后续的继承:
shared_ptr<Product> product = make_shared<Vegetable>();//错误无法调用vegetable动态类型的show_product因为没有获得继承product->show_product();
虚函数的默认实参
虚函数的默认实参由其静态类型决定:
class Product {public: ......... virtual void show_product(string extra = "Product") { cout << "product : " << product_name << "extra : " << extra<< endl; }};
注意:两个的默认形参不一致
class Food : public Product {
......... void show_product(string extra = "Food") override final { cout << "Food Prodcut name : " << this->product_name << "extra : " << extra << endl; }};
调用的时候只会按照其静态类型的默认实参:
shared_ptr<Product> product = make_shared<Food>("NEW_FOOD_PRODUCT",100,"2017-8-15");product->show_product(); //输出结果:Food Prodcut name : NEW_FOOD_PRODUCT extra : Product
还有一个重要的特性,由于C++没有类型java的super,所以我们可以使用指定运行的虚函数版本:
shared_ptr<Product> product = make_shared<Food>("NEW_FOOD_PRODUCT",100,"2017-8-15");product->show_product(); //输出结果:Food Prodcut name : NEW_FOOD_PRODUCT extra : Productproduct->Product::show_product(); //输出结果:product : NEW_FOOD_PRODUCTextra : Product
注意:其类内调用也是一样的,如果想直接使用基类的某个名称相同的成员变量或者成员函数都可以使用作用域运算符指定调用的目标
抽象基类
这个和JAVA一样都有一个抽象类,同样我们都不能为这个抽象基类进行实例化,当然和JAVA一样抽象基类也可以定义类似抽象函数的东西,C++称为“纯虚函数”,以下我们暂时将Food定义为抽象基类:
class Food : public Product {
............ virtual void show_expiration_info() = 0;};
我们定义了一个纯虚函数,其类的其他不变,此时Food已经变成了一个抽象基类。值得注意的是,我们也可以为纯虚函数提供函数体,不过一定要定义在类的外部(Food.cpp):
void Food::show_expiration_info() { cout << "保质日期 " << this->expiration_date << flush;}
当然和JAVA一样继承于抽象基类的,必须实现其纯虚函数:
class Vegetable : public Food {public:
............ void show_expiration_info(){ this->Food::show_expiration_info(); cout << " 建议存储温度 5~10" << endl; }};
访问控制符会有可能影响转换:
class A {public: int a;};
class B : private A {};
当B转换A的时候,因为MAIN函数无法获得其B中的A部分所以无法进行转换:
int main() { B b; A a; a = b; //错误无法访问}
我们可以将main函数定义为B的友元,这个问题就得以解决:
class B : private A { friend int main();};
举一反三,如果private继承的话 B的派生类也无法进行转换,如果是protected继承其派生类也可以进行转换,主要取决于目前调用者是否可以访问其B类的基类部分。
提示:友元不存在任何的继承关系,即A类友元对象也不可以访问其派生类的private或者protected成员。
使用Using重新定义继承的成员访问级别
class A {public: int a; int b; int c;};
我们可以使用Using改变某些继承的变量或者函数
class B : private A {public: using A::a;private: using A::b;protected: using A::c;};
此时main函数可以访问B类的对象的a成员变量
int main() { B b; b.a = 10;}
同理B的派生类也可以访问c和a的成员变量。
虚函数的作用域
class Base {public: virtual void func(){ cout << "Base::func" << endl; };};
class D1 : public Base {public: //由于参数不一致所以这个不是虚函数 void fcn(int a){ cout << "D1::fcn" << endl; } virtual void f2(){ cout << "D1::f2" << endl; }};
class D2: public D1 {public: //虽然和D1的参数一致,但是D1的不是虚函数,这里会隐藏D1的函数 void fcn(int a){ cout << "D2::fcn(int)" << endl; } //覆写了Base的虚函数fcn void fcn(){ cout << "D2::fcn()" << endl; } //覆写了D1的虚函数 void f2(){ cout << "D2:f2" << endl; }};
模板与泛型
模板分为两种大类型,第一种是函数模板,第二种是类模板;下面将会首先介绍函数模板;
以下是函数模板的基本形态:
template<typename T>void print(T begin, T end);int main() { vector<string> v1 = {"TONY", "CHAO", "YAN"}; print(v1.begin(), v1.end());}template<typename T>void print(T begin, T end) { for (auto p = begin; p != end; p++) { cout << *p << " " << flush; } cout << endl;}
这种就比较像JAVA的泛型,但是上述的代码是针对函数的泛型。看上去和JAVA的泛型非常类似,除此之外我们还需要注意的一个问题的是,C++这里的模板函数或者是模板类,事实上不是真正的函数或者类,只是一个模板,在我们调用这些模板的时候,它会自定根据你的的实参去推断T真正类型,然后根据此类型,生成一个函数的实例,此时我们调用的就是这个实例,这个概念尤其重要。【注:有些旧的代码是使用class来去定义模板 不是使用typename,两种写法效果一致没有必要纠结】
以下是非类型模板参数
模板可以定义非类型模板参数,即由用户传入参数去推断所需的值(这些值一定要是常亮表达式),其实我见到用到这个模板了只有数组参数,可以看看下面:
template<typename T, unsigned M, unsigned N>void printArray(T (&arrayA)[M], T (&arrayB)[N]);int main() { int a[5] = {0, 1, 2, 3, 4}; int b[3] = {9, 8, 7}; printArray(a, b);}template<typename T, unsigned M, unsigned N>void printArray(T (&arrayA)[M], T (&arrayB)[N]) { for (auto i = 0; i < M; i++) { cout << arrayA[i] << " "; } cout << endl; for (auto i = 0; i < N; i++) { cout << arrayB[i] << " "; }};
其实我上面的做法纯粹是演示这个非类型模板参数而已。
模板的inline和constexpr函数定义方式:
template<typename T, unsigned M, unsigned N>inline void printArray(T (&arrayA)[M], T (&arrayB)[N])
template<typename T, unsigned M, unsigned N>constexpr void printArray(T (&arrayA)[M], T (&arrayB)[N])
一目了然不必多说
模板的编译原理
当编译器遇到一个模板定义的时候,编译并不会生成代码,只有我们实例化一个模板的特例版本时,编译器才会生成代码。我们使用模板时,编译器才生成代码,这一特性影响我们检查到代码的错误,因为在一个模板被实例化的时候,编译器才会发现错误。
通常的情况下,当我们调用一个函数的时候,编译器只需要掌握函数的声明。类似的,当我们使用一个类类型的时候,类定义必须要是可以使用的,但是其成员函数则不必,所以我们一般会将类的定义写在头文件当中,类中的成员函数则会在源文件(cpp)中定义。
但是在模板类中则不相同,编译器为了实例化一个模板,编译器需要掌握模板函数或模板类成员函数的定义,因此,与非模板代码不同,模板的头文件通常也会包含声明和定义。【尤其注意】
由于我们在模板中的定义的所有操作,编译器基本上不管的,所以我们当使用一个泛型对象的时候,如果没有模板调用的函数或者运算符会出现错误,这个需要调用模板的使用者尤其注意。
类模板
这个就更加靠近JAVA的泛型了,以下一个例子演示基本上的模板类使用情况(使用MyShared_ptr)
template <typename T>class MyShared_ptr { T *value; int *reference_count;public: //构造函数,添加引用计数器并设置为1 MyShared_ptr(T *value) : value(value), reference_count(new int(1)) {}; //拷贝构造函数,并在引用计算器+1 MyShared_ptr(const MyShared_ptr<T> &myShared_ptr) : value(myShared_ptr.value), reference_count(myShared_ptr.reference_count) { (*this->reference_count)++; } //移动构造函数 MyShared_ptr(MyShared_ptr<T> &&myShared_ptr) : value(myShared_ptr.value), reference_count(myShared_ptr.reference_count) { myShared_ptr.value = nullptr; myShared_ptr.reference_count = nullptr; } //解引用运算符 T &operator*() { return *(this->value); } //重载解引用 T *operator->() { return this->value; } //拷贝赋值运算符 MyShared_ptr<T> &operator=(const MyShared_ptr<T> &myShared_ptr) { auto new_value = myShared_ptr.value; auto new_reference_count = myShared_ptr.reference_count; (*new_reference_count)++; (*this->reference_count)--; if (this->reference_count != nullptr && *this->reference_count <= 0) { cout << "delete value " << *this->value << endl; delete this->value; delete this->reference_count; } this->reference_count = new_reference_count; this->value = new_value; return *this; } MyShared_ptr<T> &operator=(MyShared_ptr<T> myShared_ptr) { swap(*this, myShared_ptr); return *this; } void swap(MyShared_ptr<T> &ptrA, MyShared_ptr<T> &ptrB) { using std::swap; swap(ptrA.value, ptrB.value); swap(ptrA.reference_count, ptrB.reference_count); } //赋值string只改变内容,不改变指针 MyShared_ptr<T> &operator=(const T &value) { *this->value = value; return *this; } //获得数据等同于解引用 T &getValue() { return *this->value; } //对象析构时引用计数器-1 如果没有其他引用后销毁堆内存 ~MyShared_ptr() { if (reference_count) { (*reference_count)--; if (*reference_count <= 0) { cout << "delete value " << endl; delete reference_count; delete value; } } }};
其实就是将string改成了T 没有什么技术含量,然后将一下只有string有的操作删了,最后将构造函数改成传入一个指针然后完事。
类模板当中我们的成员函数可以定义在类模板内部也可以定义在类模板外部,而且定义在类模板内部的成员函数被隐性声明为内联函数;
但是需要注意的是类模板成员函数定义在外部必须以关键字template开始,后接类模板参数列表,例子如下:
template <typename T>T& MyShared_ptr<T>::getValue() { return *this->value;}
默认情况下,一个类模板的成员函数只有当程序用到它的时候才进行是实例化。
在我们在模板类内成员函数的声明可以简写为:Object<T>简写为Object 两者是等价的
MyShared_ptr<T> &operator=(const MyShared_ptr<T> &myShared_ptr)
简写为
MyShared_ptr &operator=(const MyShared_ptr &myShared_ptr)
需要注意的是,当我们在类模板外定义成员函数的时候,就不能够这些简写,需要使用第一种写法,将模板template声明和实参带上,以下事例:
template <typename T>
MyShared_ptr &operator=(const MyShared_ptr &myShared_ptr){ ..函数体... }
同样如果在定义的函数内,作用域发生改变,所以函数体内也可以使用简写方法;
类模板和友元
如果一个模板包含一个非模板友元,则友元可以访问模板的任何实例,如果友元是自身则可以授权给友元模板的所有实例,或者只授权给特定的实例。
一对一模板友元
template<typename P>bool operator==(const MyShared_ptr<P> &a, const MyShared_ptr<P> &b) { return *(a.value) == *(b.value);}
我们使用MyShared_ptr的内部成员数据,而且是private的,但是编译器此时没有任何错误提示,但是我们在运行并产生使用的时候,实例化就会出错,无法访问到MyShared_ptr对象的内部成员数据value,所以我们需要定义一个友元,而且我们需要对相应的友元函数在头文件头部进行申明,而且我们使用到了MyShared_ptr模板类,而且声明对象的友元函数时,编译器未定义MyShared_ptr模板类,所以我们需要提前声明一个MyShared_ptr类,实际点我们上代码:
template<typename T>class MyShared_ptr;template<typename P>bool operator==(const MyShared_ptr<P> &a, const MyShared_ptr<P> &b);template<typename T>class MyShared_ptr { friend bool operator==<T>(const MyShared_ptr<T> &a, const MyShared_ptr<T> &b);
提前声明还没有定义的MyShared_ptr,然后再声明还没有跑到定义的==运算符重载模板函数,然后对象在函数中显性指定友元函数模板的实例类型。
通用函数函数模板友元如下:
template<typename T>class MyShared_ptr { template<typename P> friend bool operator==(const MyShared_ptr<P> &a, const MyShared_ptr<P> &b)
当然了,模板类友元也是一Q样的
template<typename T>class MyShared_ptr { friend class MyVector<T>;
还有针对所有实例的版本:
template<typename T>class MyShared_ptr { template <typename P> friend class MyVector;
我们还可以让自己的模板类参数成为模板类实例的友元:
template<typename T>class MyShared_ptr { friend T;
模板别名
我们可以使用typedef进行定义模板实体类的别名:
typedef MyShared_ptr<string> str_p;str_p sp(new string("TONY"));
在新的标准下允许我们使用一个模板的别名:
template<typename T>using point = MyShared_ptr<T>;int main() { point<string> sp(new string("TONY")); cout << *sp << endl;}
当然如果有多个类参数我们可以固定一个或多个类参数:
template<typename T>using key_map = map<string, T>;int main() { key_map<int> kmap = { {"A", 1}, {"B", 2}, {"C", 3} }; for(auto item = kmap.begin();item != kmap.end();item++){ cout << item->first << " <-> " << item->second << endl; }}
模板类定义static成员
注意一个重点:在模板类定义static成员,每个模板的实例都会有一个独立的static成员
template<typename T>class Foo {public: static string foo_static; //声明静态成员};template<typename T> string Foo<T>::foo_static = "TONY";
使用如下:
Foo<string> foo_str;foo_str.foo_static = "TONY";cout << Foo<string>::foo_static << endl;
使用类的类型成员:
int main() { vector<string> a; cout << getSize(a) << endl;}template<typename T>typename T::size_type getSize(const T &t) { if (t.empty()) { return typename T::size_type(0); } return t.size();}
因为模板T在编写过程之中并不知道是如何定义的,所以在实例化过程之中默认情况下,T::size_type认为是一个成员变量,而不是一个类型,所以我们需要使用typename显示定义T::size_type是一个类型。
默认模板参数:
和函数参数一样,我们也可以定义一个默认的模板类型参数;
int main() { int a = 1; int b = 2; cout << compare(a,b,greater<int>());}template<typename T, typename F = less<T>>bool compare(const T &a, const T &b, F f = F()) { return f(a, b);};
使用默认类型参数:
int main() { int a = 1; int b = 2; cout << compare(a,b);}
模板类的默认类型参数使用方式有所不同,如果我们希望使用默认的类型参数,也必须使用<>,但是只不过是参数为空而已:
template<typename T = string>class Foo {public: T value;};
使用默认模板类参数:
int main() { Foo<> foo; foo.value = "TONY"; cout << foo.value << endl;}
类的函数模板成员:
我们可以为一个普通的类,定义一个模板函数,但是我们不能将模板函数定义为虚函数:
class DeletePointLog {public: template<typename T> void operator()(T *target_p) const { cout << "delete point Log : " << *target_p << endl; delete target_p; }};
上面定义了一个删除指针的日志器,里面有一个模板运算符函数,所以支持任何类型的参数,但是由于需要输出日志,所以被删除的对象必须定义了<<运算符,使用方式如下:
int main() { unique_ptr<string,DeletePointLog> str_p(new string("TONY"),DeletePointLog());}
结束函数后自定调用析构函数,所以调用了我们定义的DeletePointLog对象;
我们还可以定义模板类的构造函数为一个模板函数:
template <typename T>class MyVector {public: template <typename IT> MyVector(const IT &begin,const IT &end);
我们接受一个迭代范围,而且迭代器的类型我们不去指定,这样我们就能够使用任意的迭代器为我们的MyVector类进行初始化,在模板作用域外进行定义的时候,采用以下写法:
template<typename T>template<typename IT>MyVector<T>::MyVector(const IT &begin, const IT &end) { elements = str_allocator.allocate(end - begin); first_free = uninitialized_copy(begin, end, elements); cap = end - begin;}
注意:这里定义了两个template,因为第一个template是类模板的template第二个为构造函数模板的template;
关于模板的类型转换:
1、一般情况下模板时不会类型转换,而是生成一个新对应的模板实例;
2、如果是数组形式一般转型为数组的首个指针,但是模板为引用则直接引用数组,但是需要注意的是数组的长度;
template<typename T>void fobj(T a,T b);template<typename T>void fref(const T &a,const T &b);int main() { int a[2] = {0,1}; int b[3] = {8,9,6}; fobj(a,b); //传入的是指针,生成为int类型的参数 fref(a,b); //传入的是const int& 虽然可以转型为const 但是重点是两个数字的大小不一样,所以不是同一个类型}template<typename T>void fobj(T a,T b){ cout << *a << endl; cout << *b << endl;}template<typename T>void fref(const T &a,const T &b){ cout << a[1] << endl; cout << b[1] << endl;}
对于引用,其实我们可以定义两个不同的模板类型参数去提高灵活性:
template<typename T1,typename T2>void fref(const T1 &a,const T2 &b);int main() { int a[2] = {0,1}; int b[3] = {8,9,6}; fobj(a,b); //传入的是指针,生成为int类型的参数 fref(a,b); //传入的是const int& 虽然可以转型为const 而且有两个不同的模板参数}
有时候我们需要显性指定函数模板参数的类型
template<typename T1,typename T2,typename T3>T1 sum(T2 a,T3 b){ return a + b;};
上面编译器是无法推断我们的返回类型应该是什么类型,所以我们必须手动指定其类型参数
long result_value = sum<long>(10,15.2);cout << result_value << endl;
上面我们只是指定了一个类型,因为其他两个类型由编译器自动推断
我们也可以使用尾置返回类型,并使用decltype自定推断返回类型:
int main() { vector<int> v1 = {1,2,3,4,5,6,}; cout << sum(v1.begin(),v1.end()) << endl;}template<typename T>auto sum(const T &begin, const T &end) -> decltype(*begin) { auto total = *begin; for (auto p = begin+1; p != end; p++) { total += *p; } return total;}
常用的类型转换标准库:
我们可以使用类型转换标准库,将一个引用获得一个去除其引用的类型:
int main() { int value = 10; int &value_ref = value; typename remove_reference<decltype(value_ref)>::type i; i = 10; cout << i << endl;}
我们将使用其模板的type成员类定义一个变量。
转型模板
若T为
则Mod<T>::type为
remove_reference<T>
X& 或 X&&
否则
X
T
add_const
X&\const X 或者函数
否则
T
const T
add_lvalue_reference
X&
X&&
否则
T
X&
T&
add_rvalue_reference
X&或X&&
否则
T
T&&
remove_pointer
X*
否则
X
T
add_pointer
X&或X&&
否则
X*
T*
make_signed
unsigned X
否则
X
T
make_unsigned
带符号类型
否则
unsigned X
T
remove_extent
X[n]
否则
X
T
remove_all_extents
X[n1][n2]
否则
X
否则T
模板实例的函数指针:
我们可以使用函数的类型指针去接受一个模板的赋值,此时会生成一个与其对应的实例进行赋值:
int main() { int (*f_p)(const int&,const int&) = compare; cout << f_p(1,1) << endl;}template <typename T>int compare(const T& a,const T& b){ if(a<b){ return -1; }else if(b==a){ return 0; } return 1;
}
但是需要注意的是,模板赋值时必须要清楚确定模板是可以生成对应的实例的。
而且我们赋值的过程中需要注意函数的重载赋值:
int main() { fun(compare); //发现有两个可匹配的函数}void fun(int(*f_p)(const int &, const int &)) { cout << "int : " << f_p(1, 1) << endl;}void fun(int(*f_p)(const string&,const string&)){ cout << "string : " << f_p("A","N") << endl;}template<typename T>int compare(const T &a, const T &b) { if (a < b) { return -1; } else if (b == a) { return 0; } return 1;}
当然我们可以使用显性的方式去进行调用:fun(compare<string>)显示调用的是fun(int(*f_p)(conststring&,const string&));
模板实参推断和引用
template <typename T> void f1(T&); //实参必须是一个左值
f1(i) i是一个int类型;模板参数类型T为int
f1(ci) ci是一个int类型;模板实参类型T为constint
f1(5) 错误,传递给一个&参数的实参必须是一个左值
template <typename T> void f2(const T&) //此时可以接受一个右值
f2(i) i是一个int类型;模板参数类型T为int
f2(ci) ci是一个constint类型;模板参数类型T为int
f2(5) 一个const&参数可以绑定一个右值;T为int
template <typename T> void f3(T&&) //此时为右值参数
f3(5) 实参是一个int类型的友值,模板参数T为int
引用折叠和右值引用参数
一般情况下一个右值引用我们是无法传入一个左值引用的,但是我们可以使用move函数去对左值进行右值的转换,但是在模板当中还有一种例外,这种例外叫做引用折叠。如f3(T&&)接受一个右值引用,但是我们传入的是一个左值int类型,此时编译器将会不会解析为int而是会解析为int&,然后就出现了这种类型int& && 一个左值引用的右则引用,引用的引用会产生引用折叠,此时会折叠成未int&一下是一些引用折叠的规则:
X& &、X& &&、X&& & 都折叠成类型X&
X&& && 折叠成类型 X&&
上述的折叠规则我们需要谨记
template <typename T> void f3(T&&);
f3(i) i是一个int类型;模板参数类型为int&,折叠为int&
f3(ci) ci是一个constint类型;模板参数类型为constint&,折叠为constint&
但是这种折叠需要注意比较严重的问题,因为有时候我们因为模板参数的不确定性导致业务错误:
template <typename T>void testing(T&& val){ T t = val; t = 10; cout << val << endl;}
上述模板当中,如果我们使用一个右值int则T是一个int类型,如果我们传入的是一个左值则T是一个int&所以会直接影响到val的结果:
int main() { int a = 20; testing(a);}
输出结果为:10;因为T为int&
int main() { testing(20);}
输出结果为20;因为T为int
上面的示例中如果想不影响业务,我们可以使用类型转换的标准库 remove_reference等
template <typename T>void testing(T&& val){ typename remove_reference<T>::type t = val; t = 10; cout << "&& : " << val << endl;}
std::move的实现原理:
int main() { int a = 10; int&& b = my_move(a);}template<typename T>typename remove_reference<T>::type&& my_move(T &&val){ return static_cast<typename remove_reference<T>::type&&>(val);}
上述结论得出从一个左值static_cast到一个右值引用是允许的。
转发
看一下以下的坑爹例子:
int main() { int a = 10; testing(fun1,10,20);}void fun1(int &a, int &b) { cout << a << " " << b << endl;}template<typename F, typename P1, typename P2>void testing(F f, P1 &&p1, P2 &&p2) { f(p2, p1);}
这个调用竟然是正确的,因为就算经过折叠,p1和p2是右值引用也好,函数的参数是属于左值,无论是变量也好函数参数也罢,右值的变量和参数本质就是一个左值,越来越懵了。。。
再看看下面的例子:
int main() { int&& a=10; int&&b = a;}
Int &&b = a 这个是会报错,报错提示为不能将一个左值赋值到一个右值引用当中:
error: cannot bind 'int' lvalue to 'int&&'
所以就会有以下的坑爹场面:
int main() { int a=10; testing(fun1,a,20);}void fun1(int &&a, int b) { cout << a << " " << b << endl;}template<typename F, typename P1, typename P2>void testing(F f, P1 &&p1, P2 &&p2) { f(p2, p1);}
虽然p1通过折叠是一个int&&,但是由于int&&是一个左值表达式,所以我们调用fun1传入参数的时候就会出现问题,异常坑爹,所以这里我们需要用到一个转发函数:
int main() { int a=10; testing(fun1,a,20);}void fun1(int &&a, int b) { cout << a << " " << b << endl;}template<typename F, typename P1, typename P2>void testing(F f, P1 &&p1, P2 &&p2) { f(std::forward<P2>(p2), std::forward<P1>(p1));}
这里的forward会将我们的参数以原来的类型进行返回,因为此时P2为int&&而且经过折叠后p2的实参为int&&但是这个时候是左值表达式,使用forward<P2>会和类型为int&&p2和P2进行折叠,返回一个右则;同理forward<P1>此时P1为int&、p1参数的类型为int& 两者经过折叠返回int&,所以forward函数有返回参数实质类型的作用。
重载与模板:
在重载过程中,模板与非模板的函数的编译器选择如下:
1、如果同样好的函数中只有一个是非模板函数,则选择此函数;
2、如果同样好的函数中没有非模板函数,而右多个函数模板,且其中一个模板比其他模板更特例化,则选择此模板;
3、其他均属于有歧义;
int main() { string a = "TONY"; const string *tp = &a; testing(tp);}template<typename T>void testing(T* val){ cout << " T*val : " << *val << endl;}template<typename T>void testing(const T& val){ cout << "T&val : " << val << endl;}
输出结果:T*val : TONY
可变参数模板:
可以鼠标的参数被称为参数包,存在两种参数包:模板参数包,表示0个或多个模板参数;函数参数包,表示0个或多个函数参数。
int main() { string t = "TONY"; double a = 12.52; int b = 23; long c = 323; string d = "TEST"; string e = "sdf"; testing(t,a,b,c,d,e);}template <typename T,typename ... Args>void testing(const T& t,const Args& ... rest){ cout << t << endl; cout << sizeof...(Args) << endl; //类型参数总数 输出5 cout << sizeof...(rest) << endl; //函数参数总数 输出5};
包参数的递归调用写法:
using namespace std;template <typename T,typename ... Args>void print_args(const T &val,const Args& ... args);template <typename T>void print_args(const T &val);int main() { string t = "TONY"; double a = 12.52; int b = 23; long c = 323; string d = "TEST"; string e = "sdf"; print_args(t,a,b,c,d,e);}template <typename T>void print_args(const T &val){ cout << val << endl;};template <typename T,typename ... Args>void print_args(const T &val,const Args& ... args){ cout << val << endl; print_args(args...);};
这里使用了一种名为包扩展的东西,即如果我们使用args...的方式等于是一个个元素单独赋值即:
print_args(t,a,b,c,d,e) 如果是直接传args就等同于转入及一整个包。
而且包扩展可以这样玩:
print_args(func(args)...) 则等价于print_args(func(t),func(a),func(b),func(c),func(d),func(e));
int main() { string t = "TONY"; double a = 12.52; int b = 23; long c = 323; string d = "TEST"; string e = "sdf"; print_args(t, a, b, c, d, e);}template<typename T>string add_header(const T &a) { std::ostrstream str_os; str_os << "header->" << a << endl; return str_os.str();}template<typename T>void print_args(const T &val) { cout << val << endl;};template<typename T, typename ... Args>void print_args(const T &val, const Args &... args) { cout << val << endl; print_args(add_header(args)...);};
包转发:
在新标准下支持包转发功能,我们在使用标准库容器时,emplace_back会将参数传递给模板实参的构造函数,我们也在MyVector当中实现这一功能:
测试的实体类,有一个带有三个参数的构造函数,类的定义如下:
class Entity {public: string str_value; double db_value; int int_value; Entity(string str_value, double db_value, int int_value) : str_value(str_value), db_value(db_value), int_value(int_value) {};};
我们在MyVector当中添加emplace_back模板函数:
template<typename T>template<typename ...Args>T *MyVector<T>::emplace_back(Args&& ...args) { checkCap(1); str_allocator.construct(first_free, forward<Args>(args)...); return ++first_free;}
我们可以看到使用forward转发函数并使用了包扩展,使用MyVector的emplace_back函数和其他标准库容器一致:
int main() { MyVector<Entity> myVector; myVector.emplace_back("TONY", 20.5, 10); cout << myVector[0].str_value << " " << myVector[0].db_value << " " << myVector[0].int_value << endl;}
使用这个forward加包扩展 从而可以让其容器的使用者可以通过emplace_back函数使用移动构造函数等。
这里需要注意其使用语法:forward<Args>(args)...
函数模板特例化
我们可以针对一些特殊的类参数定义一个特殊实例,成为特例化版本,我们一般情况下会将其模板函数的特例化版本放在同一个头文件当中:
template<typename T>void print(const T &val);template<>void print(const string &val);int main(){ string a = "TONYA"; print(a);}template<typename T>void print(const T &val){ cout << val << endl;}template<>void print(const string &val){ cout << "string : " << val << endl;}
就算是特例化也遵循最佳匹配策略:
int main(){ print("TONYA");}template<typename T>void print(const T &val){ cout << val << endl;}template<typename T ,unsigned N>void print(const T (&val_array)[N]){ cout << "char[N] : " << val_array << endl;};template<>void print(const char* const &val){ cout << "char *p : " << string(val) << endl;}
类模板的特例化
同理类的特例化,形式一致,例子如下:
template<typename T>class Foo {public: T value; void print(){ cout << value << endl; } T& getValue(){ return this->value; }};template<>class Foo<string>{public: string value; void printString(){ cout << "string : " << value << endl; } string& getValue(){ return this->value; }};
使用模式如下:
int main(){ Foo<int> foo_int; foo_int.value = 10; foo_int.print(); Foo<string> foo_string; foo_string.value = "tony"; foo_string.printString();}
注意的是foo<string>特例化,有自己不同的printString函数,而没有通用模板的print函数。
类模板部分特例化
和函数模板不同,类模板支持部分特例化操作:
template<typename T,typename V>class Foo{public: T value; V other_value; void print(){ cout << value << endl; } void print_V(){ cout << "V : " << this->other_value << endl; } T& getValue(){ return this->value; }};template<typename V>class Foo<string,V>{public: string value; V other_value; void print_V(){ cout << "other V : " << this->other_value << endl; } void printString(){ cout << "string : " << value << endl; } string& getValue(){ return this->value; }};
使用方式:
int main(){ Foo<int,int> foo_int; foo_int.other_value = 10; foo_int.print_V(); Foo<string,string> foo_string; foo_string.other_value = "tony"; foo_string.print_V();}
输出结果:
V : 10
other V : tony
需要注意的是:类模板的部分特例化本质上是一个模板而不是实例。
当然我们可以使用这种模式进行部分特例化:
emplate<typename T>
class Foo<T&>
类模板的成员特例化
我们可以根据不同的成员函数进行类的成员特例化:
template<typename T>class Foo {public: T value; void print(){ cout << value << endl; } T& getValue(){ return this->value; }};template<>void Foo<string>::print(){ cout << "string : " << value << endl;}
但是实际测试当中,发现会报错多次定义的错误:
multiple definition of `Foo<std::string>::print()'
提示:但是在visualstudio 2017 C++ 编写consoleapplication 的时候测试成功。
- JAVA 转 C++ 必记 PartC
- 成为c#、java调试高手必记的重要快捷键
- JAVA 转 C++ 必记 PartA
- JAVA 转 C++ 必记 PartB
- JAVA 转 C++ 必记 PartD
- java必记的知识点
- c/c++必学
- C语言必看!
- c编程必知
- java初学者必看
- Java入门必学
- 学习Java必看
- 学习java必看
- 学习java必看
- 初学JAVA必看
- 学习java必看
- Java面试必看
- java必看
- Python的chr()、unichr()、ord()
- 百度地图SDK,报167错误,经纬度定位是4.9E-324的解决办法
- Ant和Ivy安装部署
- 操作系统分区原理(笔记)
- three.js旋转元素
- JAVA 转 C++ 必记 PartC
- 欢迎使用CSDN-markdown编辑器
- Easy 12 Count and Say(53)
- NGINX原理分析 之 SLAB分配机制 (转)
- CB Insights:分析101个创业失败案例,我们总结了20大失败原因
- js之事件冒泡和事件捕获详细介绍
- sybase 锁定
- Swing边框用法总结(Border)一
- 解决过拟合的一些方法