0%

c++ study tips

c++ study tips

记录一些学习c++过程中的小问题或者有意思的点。

互相引用

A和B两个头文件互相引用在c++中会报错,因为可能会发生超前引用问题,超前引用是指使用了一个只声明了但没有定义的类型来声明一个变量,也就是还没有定义只声明了就使用它来创建变量,编译器是不会允许通过的。

解决办法就是在A的头文件中includeB,然后在A的.h文件中就能正常使用B了,但是在B的.h文件中不能includeA,只能声明一个class A,然后也不能声明一个A的对象比如A a,只能声明一个A的指针A *a。但是在B的.cpp文件中可以includeA来使用A的一些声明。

至于为什么可以声明指针但不能声明对象是因为此时对B来说A只声明了没有定义不知道A的大小所以没有办法确定A的大小,但是指针始终是8个字节可以确定,所以可以声明一个指针。

不得不说c++是真的饶啊。

指针类

顾名思义就是某一个类的指针类,这个指针类并不是一个真的指针,而是会封装这个类的一些指针操作,然后通过这个指针类来操作这个类的数据会更加方便一些,比如容器和迭代器就是类和指针类。

返回局部变量

众所周知函数不能返回自己的局部变量的地址,但是比较我比较疑惑的是为什么可以返回一个局部对象,经过查阅得知当返回一个局部对象的时候并不是返回局部对象本身,而是将返回对象拷贝到函数调用点,所以返回的是一个副本,这个副本的作用域不是这个函数的,而是调用这个函数的函数的。

智能指针初始化

默认初始化一个智能指针,然后使用程序就会报错,报错的原因是默认初始化的只能指针中保存着一个空指针,既然是空指针那就肯定不能使用。

1
2
3
4
5
6
int main(){
shared_ptr<vector<string>> word;
word->push_back("123");
auto beg=word->begin();
cout<<*beg<<endl;
}

使用make_shared进行初始化才可以使用,比如

1
2
3
4
5
6
int main(){
shared_ptr<vector<string>> word= make_shared<vector<string>>();
word->push_back("123");
auto beg=word->begin();
cout<<*beg<<endl;
}

非const引用与临时变量问题

首先得说明一下临时变量和局部变量的区别,局部变量指在函数内显示声明的变量称为局部变量,所以局部变量都是有变量名的,相对于的临时变量虽然也是函数内声明的变量但是这个变量没有变量名的,局部变量很容易理解,临时变量通常在函数参数传递发生类型转换以及函数返回值时被创建。

当一个函数的形参为非const类型,而一个参数以非const传入,编译器一般会认为程序员会在该函数里修改该参数,而且该参数返回后还会发挥作用。此时如果你把一个临时变量当成非const引用传进来,由于临时变量的特殊性,程序员无法对改临时变量进行操作,同时临时变量可能随时会消失,修改临时变量也毫无意义,因此,临时变量不能作为非const引用。

比如下面的代码,uppercasify()函数的参数是string类型,但是传入的是char *类型,所以会把char *隐式转化成string变量,这个string变量就是临时变量,那uppercasify()函数内部如果对string操作的化也是对临时变量string进行操作而不是对subtleBookPlug变量进行操作,这样就会引起误操作了。所以不能使用非const引用接收临时变量,不仅没有意义还会引起误操作。

但是肯定非const引用可以接收局部变量的。

1
2
3
4
5
6
7
8
9
10
11
void uppercasify(string& str) 
{}

int main(int argc, char* argv[])
{
char subtleBookPlug[] = "Effective C++";

uppercasify(subtleBookPlug);

return 1;
}

还是那句话,c++真的饶。

记录自己利用智能指针和标准库写的单词索引程序

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "TextQuery.h"

void runQueries(fstream &file){
TextQuery textquery(file);
string user_string;
while(true){
cout<<"plz input your word: ";
if(!(cin>>user_string)||user_string=="quit"){
break;
}
QueryResult qr=textquery.query(user_string);
print(cout,qr)<<endl;
}
}
int main(){
fstream file;
file.open("./2.txt",ios::in);
if(!file){
cout<<"open file fail"<<endl;
}
runQueries(file);
}

TextQuery.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

#ifndef INC_1_TEXTQUERY_H
#define INC_1_TEXTQUERY_H
#include <fstream>
#include <map>
#include <set>
#include<memory>
#include<vector>
#include<string>
#include<sstream>
#include<iostream>
#include "QueryResult.h"

class TextQuery {
public:
TextQuery(std::fstream &);
void getword();
QueryResult query(const std::string);
private:
std::shared_ptr<std::vector<std::string>> file_context;
std::map<std::string,std::shared_ptr<std::set<int>>> word;
};
#endif //INC_1_TEXTQUERY_H

TextQuery.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

#include "TextQuery.h"


TextQuery::TextQuery(std::fstream &file) {
std::string tmp;
int cur=0;
this->file_context=std::make_shared<std::vector<std::string>>();
while(std::getline(file,tmp)){
this->file_context->push_back(tmp);
std::istringstream line(tmp);

while(line>>tmp){
if(this->word.find(tmp)==this->word.end()){
auto word_set= std::make_shared<std::set<int>>();
word_set->insert(cur);
this->word.insert({tmp,word_set});
}else{
auto value=this->word[tmp];
value->insert(cur);
}
}

cur++;
}
}

void TextQuery::getword() {
auto iter=this->word.begin();
while(iter!=this->word.end()){
std::cout<<iter->first<<" ";
auto tmp=iter->second->begin();
while(tmp!=iter->second->end()){
std::cout<<*tmp<<" ";
tmp++;
}
std::cout<<std::endl;
iter++;
}
}
QueryResult TextQuery::query(std::string user_word){
auto word_set=this->word[user_word];
return QueryResult(user_word,word_set,this->file_context);
}

QueryResult.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

#ifndef INC_1_QUERYRESULT_H
#define INC_1_QUERYRESULT_H

#include<string>
#include<memory>
#include<set>
#include<vector>
#include<ostream>
#include<iostream>
class QueryResult {
friend std::ostream& print(std::ostream &os, QueryResult &qr);
private:
std::string word;
std::shared_ptr<std::set<int>> word_set;
std::shared_ptr<std::vector<std::string>> file_count;
public:
QueryResult(std::string word,std::shared_ptr<std::set<int>> word_set,std::shared_ptr<std::vector<std::string>> file_count){
this->word=word;
this->word_set=word_set;
this->file_count=file_count;
};

};

#endif //INC_1_QUERYRESULT_H

QueryResult.cpp

1
2
3
4
5
6
7
8
9
10
11
#include "QueryResult.h"
std::ostream& print(std::ostream &os,QueryResult &qr){
os<<qr.word<<" "<<"occurs "<<qr.word_set->size()<<" times"<<std::endl;
auto iter=qr.word_set->begin();
while(iter!=qr.word_set->end()){
int line_num=*iter+1;
os<<"(line "<<line_num<<")"<<" "<<(*qr.file_count)[*iter]<<std::endl;
iter++;
}
return os;
}

整体来说不是很难,主要记录一下这些标准库的主要用法。

拷贝构造函数的参数问题

拷贝构造函数的参数只能这个类的对象的引用不能是一个对象,因为当拷贝构造函数形参是一个对象时,那发生拷贝的时候就会调用拷贝构造函数,而拷贝构造函数也会发生拷贝,所以又会调用拷贝构造函数,这就造成了死循环了。这个拷贝构造函数永远没有办法调用成功。

注意参数不仅是引用,如果要使用容器对对象进行存储的话,必须得存在一个const引用的构造函数,原因应该还是非const引用没办法接收临时变量。

所以拷贝构造函数最好有两个版本,一个const引用一个非const引用(我的理解)。

explicit

编译器可以自动隐式的进行一步类型转换,但只能进行一步,比如下面代码

1
2
3
4
class A{
A(string s);
};
void fun(A a);

如果这样fun("123"),就会发成错误,因为编译器只能自动隐式的进行一步类型转换,比如把字符串转换成string或者把string转换成class A,但是不能自动进行两步转化,fun("123")就是两步转化,正确的方式可以是这样fun(string("123"))或者fun(A("123"))

这确实很方便,但是有时候程序员并不想进行这种自动转化,那就可以使用explicit进行限制,比如

1
2
3
4
class A{
explicit A(string s);
};
void fun(A a);

此时再调用fun(string("123"))就会发生错误,因为禁止从string转化成class A了。

拷贝初始化

拷贝初始化的时候会自动调用拷贝构造函数或者移动构造函数。

直接初始化和拷贝初始化还是有很大区别的,直接初始化指调用构造函数完成对象的初始化工作,拷贝初始化是指当对象发生拷贝的时候被动调用拷贝构造函数或者移动构造函数。

拷贝构造函数和拷贝赋值运算符的区别

主要区别就是他们调用的时机不同,比如

1
2
3
4
5
6
7
8
9
10
11
12
13
class A{
public:
int s;
A(){
cout<<"asd"<<endl;
}
A(A &a){
cout<<"123"<<endl;
};
A& operator=(const A&){
cout<<"234"<<endl;
};
};

如果这样

1
2
3
4
int main(){
A a;
A c=a;
}

那就是调用拷贝构造函数,输出123,那是因为此时是初始化一个对象的时候发生拷贝行为,就会调用拷贝构造函数

如果这样

1
2
3
4
int main(){
A a,c;
c=a;
}

单纯的把一个已经初始化的对象赋值给另一个已经初始化的对象就会调用拷贝赋值运算符。

所以为了一个类的健壮性,建议两者都得有。

小记

c++太恐怖了,太多细节了。

智能指针的简单实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class HasPtr{
friend void swap(HasPtr &,HasPtr &);
public:
HasPtr(int i,const string &s= string()):ptr(new string(s)),i(i),use(new size_t(1)){};
HasPtr(const HasPtr &hasptr):ptr(hasptr.ptr),i(hasptr.i),use(hasptr.use){
*use++;
};
HasPtr(HasPtr &hasptr):ptr(hasptr.ptr),i(hasptr.i),use(hasptr.use){
*use++;
};
HasPtr& operator=(const HasPtr &hasptr){
(*hasptr.use)++;
*use--;
if(*use==0){
delete use;
delete ptr;
}
ptr=hasptr.ptr;
use=hasptr.use;
i=hasptr.i;
return *this;
};
bool operator<(HasPtr &hp){
if(i>hp.i){
return true;
}else{
return false;
}
};
string& getstring(){
return *(this->ptr);
};
~HasPtr(){
*use--;
if(*use==0){
delete use;
delete ptr;
}
};
private:
size_t *use;
string *ptr;
int i;
};
inline void swap(HasPtr &lhs,HasPtr &rhs){
cout<<"hp swap"<<endl;
swap(lhs.use,rhs.use);
swap(lhs.ptr,rhs.ptr);
swap(lhs.i,rhs.i);
}

const对象只能调用const成员函数

因为要保证对象的const属性.

std::move

标准库函数,能够显示调用对象的移动构造函数。

比如下面的代码

1
2
3
4
int main(){
string s1="123";
string s2(move(s1));
}

就是将s1移动到了s2,移动后s1依然可以正常析构,但是此时s1不再指向”123”的字符串了,s2将指向”123”的字符串。

multiple definition 多重定义问题

在c++中切记切记不要在头文件中定义全局变量或者函数,因为如果这个头文件被多个cpp文件引用绝对会爆multiple definition这个错。

如果在头文件中定义了变量(是定义不是声明),并分别在a.c和b.c中进行了引用,编译过程中这个变量的符号会同时包含在a.o和b.o中,导致链接失败,原因是C语言规定“一个变量可以多次声明但只能定义一次”,解决办法是在头文件中加上#ifndef X条件编译,使该变量只定义一次,但是这里又有一个问题,该解决办法只适用C而不适用C++,在C++中,即使在头文件中加了#ifndef X,链接错误同样会发生,原因是C++中#ifndef X的作用域仅在单个文件中,因此只要在.h中定义了变量并在不同.cpp中进行引用,链接时都会报重定义错误,再说得直白点,a.cpp和b.cpp都引用了条件编译的g.h,g.h的条件编译只能分别保证在a.cpp和b.cpp中不出现重复定义,但在链接a.o和b.o的过程中就会发现重复定义。

c++这样做我理解是更加细粒度变量的作用域,变量只属于某一个模块而不是整个程序,如果一个模块想使用另一个模块的某一个变量就得使用extern这个关键字

extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。要使用其他模块的变量只要在这个模块中使用extern 变量声明就可以了。

StrVec简单实现

.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#ifndef INC_1_STRVEC_H
#define INC_1_STRVEC_H
#include<string>
#include<memory>
#include<utility>
#include<initializer_list>
class StrVec {
public:
StrVec():elements(nullptr),first_free(nullptr),cap(nullptr){};
StrVec(std::initializer_list<std::string>);
StrVec(const StrVec&);
StrVec& operator=(const StrVec&);
std::string& operator[](int );
~StrVec();
void push_back(const std::string&);
size_t size(){
return first_free-elements;
};
size_t capacity(){
return cap-elements;
};
std::string* begin() const{
return elements;
};
std::string* end() const{
return first_free;
};
void resize(size_t,const std::string& s=std::string(""));
void reserve(size_t);
private:
static std::allocator<std::string> alloc;
void chk_n_alloc(){
if(size()==capacity()){
reallocate();
}
};
std::pair<std::string*,std::string*> alloc_n_copy(const std::string*,const std::string*);
void free();
void reallocate();
std::string *elements;
std::string *first_free;
std::string *cap;
};


#endif //INC_1_STRVEC_H

.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include "StrVec.h"

std::allocator<std::string> StrVec::alloc;
void StrVec::push_back(const std::string &s) {
chk_n_alloc();
alloc.construct(first_free++,s);
}

std::pair<std::string *, std::string *> StrVec::alloc_n_copy(const std::string *b, const std::string *e) {
auto data=alloc.allocate(e-b);
return {data, std::uninitialized_copy(b,e,data)};
}

void StrVec::free() {
if(elements){
for(auto p=first_free;p!=elements;){
alloc.destroy(--p);
}
alloc.deallocate(elements,cap-elements);
}
}
StrVec::StrVec(const StrVec & strvec) {
auto newstrings= alloc_n_copy(strvec.begin(),strvec.end());
elements=newstrings.first;
first_free=cap=newstrings.second;
}

StrVec &StrVec::operator=(const StrVec &strvec) {
auto newstrings= alloc_n_copy(strvec.begin(),strvec.end());
free();
elements=newstrings.first;
first_free=cap=newstrings.second;
return *this;
}

void StrVec::reallocate() {
auto newvecsize=size()? size()*2:1;
auto newelement=alloc.allocate(newvecsize);
auto dest=newelement;
auto src=elements;
while(src!=first_free){
alloc.construct(dest++,std::move(*src++));
}
free();
elements=newelement;
first_free=dest;
cap=elements+newvecsize;
}
StrVec::~StrVec() {
free();
}

void StrVec::resize(size_t newsize, const std::string &s) {
auto newelement=alloc.allocate(newsize);
auto dest=newelement;
auto src=elements;
for(int i=0;(i<newsize)&&(src!=first_free);i++){
alloc.construct(dest++,std::move(*src++));
};
free();
elements=newelement;
first_free=dest;
cap=elements+newsize;
}
void StrVec::reserve(size_t newsize) {
if(newsize>capacity()){
resize(newsize);
}
}
StrVec::StrVec(std::initializer_list<std::string> lst) :elements(nullptr),first_free(nullptr),cap(nullptr){
for(auto iter=lst.begin();iter!=lst.end();iter++){
push_back(*iter);
}
};

std::string &StrVec::operator[](int idx) {
return *(elements+idx);
}

左值 右值 左值引用 右值引用

(93条消息) 【C++】左值和右值、左值引用(&)和右值引用(&&)_Jacky_Feng的博客-CSDN博客_c++ 左值右值&&的作用

比较有意思的点是右值引用变量是左值。所以不能把一个右值引用变量赋值给一个右值引用变量。因为右值引用变量是一个变量,在内存中有对应地址,所以他本身并不是一个右值,而是一个左值。

类的小知识点总结

  • 动态绑定就相当于多态,多态不仅可以通过指针来使用,还可以通过引用

  • 虚函数也可以在自己的类中进行定义

  • 析构函数是可以虚函数的,在基类中通常就应该定义一个虚析构函数。

  • 任何构造函数之外的非静态函数都可以是虚函数。

  • 派生类必须将继承而来的成员函数中需要覆盖的那些重新声明。

  • 派生类如果是public继承了类,那他就只可以访问父类的公有成员和受保护成员0,不可以访问私有成员。

  • 如果派生类的虚函数需要使用默认实参,基类和派生类中定义的默认实参最好一致,不然通过多态调用派生类的虚函数的时候,传入的默认参数是基类的默认参数。

  • 派生类可以重写或者不重写基类的虚函数,但是纯虚函数派生类必须得重写,除非派生类也是一个抽象基类。

  • protected是publibc和private的中和产物,当使用protected修饰成员的时候,这个成员就是受保护成员,这个成员对于类的用户来说不可见,但是对类的派生类可见。

  • 对于访问权限和继承来说,有两个影响变量,可以通过三个角度来讨论,即类的使用者,类是实现者,类的派生类。解释起来有些麻烦,忘了还是直接看书吧,p542处.

  • 派生类的作用域位于基类作用域之内,在如果调用类的某个成员函数时首先进行名字匹配,从这个类找起,如果这个类没有那就从这个类的父类找起,如果没有找见那就一直找到这个继承链的顶点,如果还没有找到那就报错,如果名字匹配上了,那就再进行类型检查,检查通过了就是合法调用。

  • 比较有意思的是派生类不存在对基类的重载,如果派生类的变量或者函数的名字和基类重了那派生类就会隐藏基类的重名函数或者重名变量,那其实重名变量在内存中有两份了,必须得通过作用域运算符来强制访问,下面的代码就是很好的解释,值得注意的是名字查找优于类型查找,如果派生类中的成员函数只有名字和基类函数重名,类型完全不一样,派生类还是会隐藏基类的函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class A{
    public:
    int c;
    A(){
    c=14;
    cout<<":"<<c<<endl;
    }
    };
    class B: public A{
    public:
    int c;
    B(int c):c(c){};
    void get_c(){
    cout<<A::c<<endl;
    }
    };
    int main(){
    B b(23);
    b.c=12;
    cout<<b.c<<endl;
    b.get_c();
    cout<<b.c<<endl;
    }
  • 如果基类中有一个函数名的多个重载版本,在派生类中还想重载某一个版本,不能直接对其重载,原因上述已经说清楚了,可以使用using把所有的重载版本全部引入派生类中再进行重载。

  • 一条经验准则,如果一个类需要析构函数,那么它也同样需要拷贝和赋值操作。但是基类的虚析构函数是个例外。原因仔细想想也能明白,正常一个类需要析构函数是因为有自己管理的资源了,有自己管理的资源那就得考虑拷贝和赋值移动的时候到底该怎么处理,但是基类不一样,基类有析构函数是因为要为多态服务,他自己可能有资源可能没有,所以可以不必要有拷贝赋值移动操纵。

  • 虚函数和多态是强绑定的,虚函数并不是说基类有虚函数,派生类就必须实现虚函数,这是错误的观点,虚函数我感觉就是完全为了多态服务,当基类的指针或者引用指向了派生类时,如果调用了虚函数,就会调用指向的对象的函数。所以如果派生类都有某个功能但是实现的方式不一样的话就可以标记成虚函数,除此之外就没必要了。

  • 当一个类中有了析构函数就不会合成移动操作,基类一定有析构函数,所以正常情况下一定没有合成的移动操作,这就会阻止派生类拥有自己的移动操作,所以如果派生类确实需要自己的移动操作,就需要在基类中显示的定义移动操作。

  • 不管是移动,构造,还是拷贝,派生类都会在初始化列表中调用基类的对应函数,以此来移动,构造或者拷贝派生类中的基类部分,然后在函数体中移动,构造,或者拷贝派生类自己的部分。

  • 在析构函数执行完之后,对象的成员会被隐式销毁,类似的,对象的基类部分也是隐式销毁,所以没必要在派生类的析构函数中调用基类的析构函数。

  • 最后不要在基类的特殊函数中使用虚函数,因为如果是派生调用基类的特殊函数的时候基类的特殊函数里的虚函数就会执行派生类的虚函数版本,这会导致不可知的错误。如果需要使用的话最好指明虚函数版本。

第十五章 单词查询程序

通过这个练习让我更加深刻的理解了类的继承的实际意义,对于要处理某一类事情,这类事情又可以分成很多小类型的话,那就可以提取所有小类型的公共特性然后成为一个基类,这些小类型继承这些基类,如果这些小类型还可以再细分的话,再次对小类型的所有小类型提取共有特性成为一个基类,小类型的小类型继承这个基类,依次类推。

这么做我感觉最大的好处就是让整个继承体系条理分明,层次关系清晰,还减少了代码的冗余量。

要处理某一类事情不能直接使用我们构造的继承体系,因为最后派生类你改使用哪一个派生类呢,所以得再构造一个类来管理使用这个继承体系,这个类(即Query)就是这个继承体系的接口类,暴露了这个继承体系的接口又隐藏了整个继承体系。

下面是这个联系的代码

Query.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef INC_1_QUERY_H
#define INC_1_QUERY_H
#include "TextQuery.h"
class Query_base;
class Query {
friend Query operator~(const Query &);
friend Query operator&(const Query &,const Query &);
friend Query operator|(const Query &,const Query &);
public:
Query(const std::string &s);
QueryResult eval(TextQuery &t) const;
std::string rep() const;
private:
Query(std::shared_ptr<Query_base> query) :q(query){};
std::shared_ptr<Query_base> q;
};
#endif //INC_1_QUERY_H

Query.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include "Query.h"
#include "Query_base.h"
std::ostream& operator<<(std::ostream &os,const Query &query){
return os<<query.rep();
}

QueryResult Query::eval(TextQuery &t) const {
return q->eval(t);
}
std::string Query::rep() const {
return q->rep();
}
Query::Query(const std::string &s):q(new WordQuery(s)){}

Query operator~(const Query &q) {
// this->q=std::shared_ptr<Query_base>(new NotQuery(*this));
// return *this;
return std::shared_ptr<Query_base>(new NotQuery(q));
}

Query operator&(const Query &l,const Query &r) {
// this->q=std::shared_ptr<Query_base>(new AndQuery(*this,r));
// return *this;
return std::shared_ptr<Query_base>(new AndQuery(l,r));
}

Query operator|(const Query &l,const Query &r) {
return std::shared_ptr<Query_base>(new OrQuery(l,r));
}

Query_base.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#ifndef INC_1_QUERY_BASE_H
#define INC_1_QUERY_BASE_H

#include "Query.h"
#include <algorithm>
#include <iterator>
class Query_base{
friend class Query;
protected:
virtual ~Query_base()=default;
private:
virtual QueryResult eval(TextQuery&) const=0;
virtual std::string rep() const =0;
};

class WordQuery:public Query_base{
friend class Query;
WordQuery(const std::string &s) :query_word(s){};
QueryResult eval(TextQuery &t) const {
return t.query(query_word);
};
std::string rep() const{
return query_word;
};
std::string query_word;
};

class NotQuery:public Query_base{
friend Query operator~(const Query&);
std::string rep() const{
return "~("+query.rep()+")";
}
QueryResult eval(TextQuery &t) const;
Query query;
protected:
NotQuery(const Query &q): query(q){}
};

class BinareyQuery : public Query_base{
protected:
BinareyQuery(const Query &l,const Query &r,const std::string &s):lhs(l),rhs(r),opSym(s){};
std::string rep() const{
return "("+lhs.rep()+" "+opSym+" "+rhs.rep()+")";
}
Query lhs,rhs;
std::string opSym;
};
class AndQuery:public BinareyQuery{
friend Query operator&(const Query &,const Query &);
AndQuery(const Query &l,const Query &r): BinareyQuery(l,r,"&"){};
QueryResult eval(TextQuery&) const;
};
class OrQuery:public BinareyQuery{
friend Query operator|(const Query &,const Query &);
OrQuery(const Query &l,const Query &r): BinareyQuery(l,r,"|"){};
QueryResult eval(TextQuery&) const;
};
#endif //INC_1_QUERY_BASE_H

Query_base.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include "Query_base.h"

QueryResult NotQuery::eval(TextQuery &t) const {
auto result=query.eval(t);
auto ret_lines= std::make_shared<std::set<int>>();
auto beg=result.begin,end=result.end;
auto size=result.get_file()->size();
for(auto n=0;n!=size;n++){
if(beg==end|| *beg!=n){
ret_lines->insert(n);
}else if(beg!=end){
++beg;
}
}
return QueryResult(rep(),ret_lines,result.get_file());
}

QueryResult OrQuery::eval(TextQuery &test) const {
auto letf=lhs.eval(test);
auto right=rhs.eval(test);
auto ret_lines=std::make_shared<std::set<int>>(letf.begin,letf.end);
ret_lines->insert(right.begin,right.end);
return QueryResult(rep(),ret_lines,letf.get_file());
}

QueryResult AndQuery::eval(TextQuery &test) const {
auto letf=lhs.eval(test);
auto right=rhs.eval(test);
auto ret_lines=std::make_shared<std::set<int>>();
std::set_intersection(letf.begin,letf.end,right.begin,right.end,std::inserter(*ret_lines,ret_lines->begin()));
return QueryResult(rep(),ret_lines,letf.get_file());
}

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
#include "Query_base.h"
int main(){
fstream file;
file.open("./2.txt",ios::in);
if(!file){
cout<<"open file fail"<<endl;
}
Query q=Query("is") & Query("that")|Query("serious");
TextQuery textquery(file);
auto res=q.eval(textquery);
print(cout,res)<<endl;
}

结果

image-20230204002217162

运算符类

运算符类是对运算的一个扩展吧相当于,可以有如下代码

1
2
plus<int> intadd;
int s=intadd(10,20);

不再是直接使用运算符,而是做了一层封装,这么做的好处就是让运算更加适普。比如两个指针的<操作,不能直接p1<p2这会出问题,但是可以这样

1
2
less<char *> intptrless;
bool s=intptrless("123","2324");

这种运算符类是标准库提供的。

使用运算符类进行模板编程会更加类型无关和可移植性。

关于模板函数要在头文件中进行定义的问题

之前讨论过普通变量或者普通函数能否在头文件中定义的问题,答案是否定的,这是非常愚蠢的行为,但是这条规则在模板编程中不适用,当编译器遇到一个模板定义的时候,他并不会生成代码,只有在当实例化模板的一个特定版本时,编译器才会生成代码,当我们使用而不是定义模板的时候,编译器才会生成代码,这一特性非常重要。假如有如下文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// add.h
template <typename T>
T add(const T &a, const T &b)
{
return a + b;
}

// main.cpp
#include "add.h"
int main()
{
int i = add(1, 1);
return 0;
}

main.cpp引用了add.h,当使用add(1,1)的时候就需要实例化一个add(int,int)的函数,模板就定义在头文件中,所以可以直接通过头文件实例化,但是如果只有定义在头文件中,而定义在cpp文件中,在链接期间就会报错了。如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// add.h
template <typename T>
T add(const T &a, const T &b);

// add.cpp
#include "add.h"
template <typename T>
T add(const T &a, const T &b)
{
return a + b;
}

// main.cpp
#include "add.h"
int main()
{
int i = add(1, 1);
return 0;
}

main函数想要寻找add模板函数并且实例化的时候,发现头文件中只有声明,他就会认为这个函数会在add.cpp中进行实例化,到时候在链接期间进行链接就好了,但是在编译add.cpp这个模块的时候又没有使用add模板函数,也就不会给他实例化,导致最终链接期间,main模块想要链接add模块中的函数,但是add模块没有这个函数,导致链接错误。

那直接在头文件中定义一个模板函数会不会像在头文件中定义一个普通函数那样,在链接的时候爆多重定义的错误。答案是不会的,因为针对特定类型模板函数长的是一样的,所以如果多个模块中都有同一个特性类型的模板函数,那就会随机选择一个并使用。

保证传递给模板的实参支持模板所要求的操作,以及这些操作在模板中能正常工作,是调用者的责任。

typedef using

typedef主要是起别名的关键字,using除了可以引入命名空间外,还可以起别名,而且还可以给模板起别名,但是typedef不能给模板起别名

1
2
3
4
5
template <typename T>
using Vec = MyVector<T, MyAlloc<T>>;

// usage
Vec<int> vec;

::作用域运算符

众所周知::可以所以类内的static成员或者类内定义的类型,对于一个非模板类来说,通过::引用的是类内的static成员还是类型很好判断,但是如果使用模板类型参数类的名字就不好判断了,在默认情况下c++假定通过::访问的名字不是类型,因此如果使用一个模板参数的类型成员,就得显示的通过关键字typename来实现,如下

1
2
3
4
5
6
7
8
template <typename T>
typename T::value_type top(const T& c){
if(!c.empty()){
return c.back();
}else{
return typename T::value_type();
}
}

模板实例化声明和实例化定义

实例化声明语法如下

1
2
extern template class Blob<string>;
extern template int compare(const int&,const int&);

这就是两个实例化声明,实例化声明是指别的模块中已经实例化了Blob所以没必要在这个模块中再实例化一次,所以进行一个声明,在编译的时候就不会实例化了而是在别的模块中找到这个实例化模板。

实例化定义语法如下

1
2
template class Blob<string>;
template int compare(const int&,const int&);

实例化定义是指在这个模块中根据模板实参实例化这个模板。此定义非彼定义。

对于每个实例化声明,在程序中的某个位置必须有其显示的实例化定义。

将实参传递给带模板类型的函数形参时,能够自动应用的类型转换只有const转换以及属猪或者函数指针的转换,但是如果函数参数类型不是模板参数,则对实参进行正常的类型转换。

引用折叠和右值引用参数

总结,如果一个函数参数是指向模板参数类型的右值引用,则可以传递给他任意类型的实参,如果将一个优质传递给这个的参数,则函数参数被实例化为一个普通的左值引用。

这个就是c++的例外,不过比较有意思的一点是模板参数可以推断为一个引用类型。

显示的左值引用转化成右值引用

1
2
std::string t="qwe";
std::string &&s=static_const<string&&>(t);

可变参数函数模板

利用递归一个一个处理参数

1
2
3
4
5
6
7
8
9
10
11
12
template <typename T>
ostream &print(ostream &os,const T &t){
return os<<t;
}
template <typename T,typename... Args>
ostream &print(ostream &os,const T &t,const Args&... rest){
os<<t<<", ";
return print(os,rest...);
}
int main(){
print(cout,2134,3456,"asfd","2134",123.2345);
}

c++基础学习告一段落