第八十一讲 再探OI


ok,最近都比较忙,所以没时间打理,好吧,今天抽空来继续探索IO,上一讲我们说过来今天的内容会比较深奥难懂一些,当然大家也不要担心的了,多看几遍还是能够看得懂的。

好,继续我们的主题,我们来看看下面的代码:
————————–
int num;


cout<<"

Pls enter a num:"

;


cin>>num;


cout<<endl;


—————————

这几句代码不难理解,而且是最常见的了,但是大家可能一直忽略了一个问题,cout是带有缓冲区的输出流,什么意思呢?简单点说就是我们要输出的文件会先被写入缓冲区中,当缓冲区满了之后才会被写出,也就是说只有当我们要输出的内容占满cout的缓冲区时他们才会出现在我们的面前,至于这个缓冲区是什么样的,他有多大我并不知道,毕竟我也没去研究过iostream的源码(不同的编译器实现方案不一样),但是这部妨碍我们对IO的探究。

根据上面我们所知道的理论,如果说当"

Pls enter a num:"

没有将缓冲区占满,那么我们可能就收不到这句提示了,这是不是意见很糟糕的事,也许我们可能会一直想在等待,等待这句提示然后我们才会想起输入一个整数。如果这还不够糟糕,那么什么才算是糟糕?不过好在iostream并不是没有给我解决方案,iostream里面的flush就是强行刷新缓冲区的,所以我们可以这样用:
——————————–
cout<<"

Pls enter a num:"

<<
flush;


———————————
ok,总算进入正题了,我们就从flush开始,假设我们的flush像下面这样,我们暂时抛开我们对flush的理解,我们来看看一个新鲜的:
———————————–
ostream&

flush(ostream&

file);


———————————–
假设这个flush的存在就是为了清除缓冲区,那么我们可以这样来使用他了:
——————————-
cout<<"

Pls enter a num:"

;


flush(cout);


——————————

当如,如果我们想要写得更加紧凑一些,我们可以这样写:
——————————–
flush(cout<<"

Pls enter a num:"

);


——————————–
当然这没关系的了,但是如果我们多输出几个呢????
———————————
cout<<1;


flush(cout);


cout<<2;


flush(cout);


cout<<3;


flush(cout);


———————————

如果我们再将上面的代码紧凑一些呢?
———————————–
flush(flush(flush(cout<<1)<<2)<<3);


———————————–

看上去有些复杂难解,是的,我第一次看到这玩意的时候我也觉得难以理解,这才三个如果再多一些那不是要让人眼花缭乱了吗?ok,我们可以换种方法来解决这个问题,假设我们有这么一个空类,然后让这个空类重载operator<<操作符:
———————————–
class FLUSH{
};


ostream&

operator<<(ostream&

os,FLUSH f){

return flush(os);


}
FLUSH _flush;


———————————

这个空类啥都不做,这不是废话吗?都说了空类当然是啥都不做的了,他的存在就是为了简化刚才我们上面那种操蛋的写法,所以现在我们已经给他定义了一个全局对象_flush,那么我们可以简单把刚才那种操蛋的写法简化成如下这样:
————————————–
cout<<1<<_flush<<2<<_flush<<3<<_flush;


————————————–

这个方法说实在的没啥好谈的,简单点说就是将flush(cout),所以,当我们写出cout<<_flush的时候他调用的是ostream&

operator<<(cout,_flush),然后这个操作符封装了我们真正的flush(cout),所以他能够很好的工作,但是真的没啥吸引力,因为他太过极限,说到吸极限,我们来看看另一种方法,这一次我们不用空类来表示:
—————————————
ostream&

operator<<(ostream&

os,ostream&

(*func)(ostream)){

return (*func)(os);


}
—————————————

这里我们使用了函数指针的东西,*func不代表某个具体的函数,他代表着一类的函数——以ostream为参数,返回ostream的引用的函数,有了上面这东西,我们再不用使用那些无聊的伪对象了,现在我们可以直接这样用:
—————————————
cout<<1<<
flush<<2<<
flush<<3<<
flush;


—————————————

上面操作能够成功的奥妙就是这里:
——————————
cout<<_f == _f(cout)
——————————-

如果不明白,我再简单的说一下上面的调用机制:当cout<<_f时,(如果_f满足我们所说的*func)这里其实调用就是ostream&

operator<<(cout,_f);

现在该是明白了吧。

我们再把情况更加复杂化一些,加入我们有一个函数,将一个long对象转换为二进制表示:
————————————
bitset<10>get(long i){

bitset<10>b(i);

//这是个有问题的设计,但是不影响我们这里的使用,如果大家有兴趣探索,可以先去看看bitset这个class。

return b;


}
————————————-
如果我们现在想要这样使用:
————————————
cout<<get(150)<<"

"

<<get(180);


————————————
现在摆在我们面前的问题是怎么阻止我们先对150求值然后保存结果后再对180求值保存结果然后再调用这几个operator<<呢?因为如果这样做的话,打印出来的结果将是错误的,为什么呢?因为连续两次调用get返回的指针指向同一块内存,所以无论是先对那一个求值,结果都会一个覆盖一个,当然或许我们可以考虑使用环形缓冲区结构来存放结果,但是这样能够解决暂时的问题,当连续多次调用后这个环形缓冲区会重新回到起点,还是会被覆盖。

上面我们用函数指针来解决flush的问题,现在我们用函数对象来解决这个问题:
—————————————-
class obj{
public:

typedef ostream&

(*_Func)(ostream&

,long);



obj(ostream&

(*func)(ostream&

,long),long v):

_func(func),_val(v){ }

ostream&

operator()(ostream&

os) const{

return (*_func)(os,_val);


}
private:

_Func _func;



long _val;


};


——————————————
现在,我们再来重载一下operator<<:
——————————————
ostream&

operator<<(ostream&

os,const obj&

_ob){

return _ob(os);


}
——————————————

最后我们再来看看关键部分:
—————————————–
ostream&

binaryconve(ostream&

os,long n){

return os<<get(n);


}
—————————————–
这里是安全的,因为我们一旦获得get(n)就马上使用,所以不会造成缓冲区被覆盖的问题。最后我们再重载binaryconve来生成一个obj对象:
—————————————–
obj binaryconve(long n){

return obj(binaryconve,n);


}
——————————————

ok,到这里,我们可以放心的这样用了:
————————————–
cout<<get(150)<<"

"

<<get(180);


————————————–

现在我们来总结一下:

当我们输出的是无参数的情况时:

cout<<
f
可以转换为f(cout),而如果我们输出多参数的时候:

cout<<
f(a0,a1,a2….an)
可以转换为f(cout,a0,a1,a2…an).

这一讲的内容有些无法理解,可能需要多看几遍多思考一阵才会明白是怎么回事,当然了,这是一些很底层的实现方案,标准库里面的很多实现就是根据这一思想得来的。

第八十一讲 再探OI

============================
回复D直接查看目录

原文始发于微信公众号(

C/C++的编程教室

):第八十一讲 再探OI

|

第八十讲 探索OI

其实关于C++的内容不打算再说的了,所以这些天都没有推送,因为我在考虑接下来我们是该说COM呢还是MFC,因为在一开始的时候打算说MFC的,那时候我没有纠结过,后面我又想,MFC的局限性太大了,如果这样的话岂不是把大家捆绑在微软上面了吗?后来想了下,COM是不错的选择,虽然他是微软提出来的,但是他是一种与语言无关性的东西,我们可以用VB,抑或是C#,还是Java等等来开发COM组件,OK,废话不多说了,不管是MFC还是COM,我们现在都没时间来说,我们现在还是继续来看看C++吧,因为不管是MFC还是我们接下来要说的COM都和C++脱不了关系(因为我只会C++,所以就算说COM,我们也是用C++来开发)。

相信如果从头开始关注我们课堂的朋友也应该明白,其实我们C++的真正内容也没啥好说的,但现在想起来,上次留给大家一个问题,ok,我来重复一遍我们的问题:

—————————————-

在不使用iostream的时候我们怎么让下面的代码运行起来:

#include <stdlib.h>

int main(){

My_IO out;

MyInt a=5;

out<<a<<"

n"

;

//在控制台打印出5然后回车。

FILE* f(fopen("

Test.txt"

,"

w+"

));

My_IO fout(f);

fout<<a<<"

n"

;

//创建一个Test.txt文件,然后把5写进文本中;

return 0;

}

——————————————

这个问题看上去有些麻烦,其实只要把思路理清了也就那么回事,首先我们来看看C++到底是什么,最直接的是C++是C的超集,所以C++的很多东西都是C扩展而来的,他填了C的很多抗但同时也带来了新坑,不过这不是我们这里该讨论的问题,我们继续来看看这个问题。

MyInt是我们自定义的一个数据结构,从上面的内容,我们可以看得出,他就是封装了C++的int,这个MyInt是以前研究多线程的时候封装的一个class,大致内容如下:

—————————————–

#ifndef _MY_INT_H_

#define _MY_INT_H_

#include "

MyLock.h"

#include "

Myiostream.h"

using namespace My_Code;


class MyInt{

public:

MyInt();

MyInt(const MyInt&

);

MyInt(const int&

);

MyInt&

operator=(const MyInt&

);

MyInt&

operator=(const int&

);

MyInt operator–();

MyInt operator–(int);

const
int Get() const;

friend bool operator==(const MyInt&

_m1, const MyInt&

_m2);

friend bool operator<(const MyInt&

_m1, const MyInt&

_m2);

friend bool operator>(const MyInt&

_m1, const MyInt&

_m2);

friend Base_IO&

operator<<(Base_IO&

out, const MyInt&

m1);

private:

int _m;

CriticSection _safelock;

//这是我封装了的一个线程安全锁,反正以后会和大家说多线程的,所以现在知道这么回事就行了。

};

#endif

——————————————

大家注意到了,我们上面的#include "

Myiostream.h"

,重点就是这里了,可能大家会觉得奇怪,会想,我们是不是哪根筋出问题了,标准库里面有那么好用的iostream,干嘛还吃饱了没事来弄个io呢?那么我们换种想法,如果我们自己写了个类库,别人想要用我们的类库的时候,他们就不得不得连入iostream,这在对效率要求高的情况下是不可忍的,而且cout不是线程安全的,所以很多时候可能自己写个oi是不错的选择。

说了这些,那么我们还是没有说io到底是啥东西,好吧,当然我们这里不讨论i,我们来讨论o,o大家都知道那是叫输出,但是有些朋友没有想到另一层含义,o其实就是把东西写到某个地方,如果想到这一层想要写个io也就简单了,可以把他想象成下面这样:

—————————-

_f.send(void* _p);

—————————-

假设这个函数实现的目的是将_p的内容写到_f里,我们这里就假如是这样的,所以,按照oo思想,我们用一个抽象类来表示接口(以便以后我们需要派生其他类呢?),然后派生出我们需要的类,假如这个类暂时就是我们所需要的io了,那么我们将这个类封装我们想要去的地方(当然是我们要把数据写进去的目的地了),大家还记得FILE吗?如果忘记的话,回去看看C语言部分的文件的读写部分,那么我们的io可以如下:

——————————-

#ifndef _My_IOSTREAM_H_

#define _My_IOSTREAM_H_

#include <stdlib.h>

#include <stdio.h>

namespace My_Code{

class Base_IO{

public:

virtual ~Base_IO(){ }

virtual
void Send(const char*) = 0;

};


class My_IO :public Base_IO{

public:

My_IO(FILE* f=stdout) :_f(f){ } //_f有默认值,就是标准输入接口。


void Send(const char* str){

fputs(str, _f);

}

private:

FILE* _f;

};

}

#endif

————————————–

到这里,我们的程序可以完美运行了。

ok,今天先这样吧,接下来我们会说说更难一点的,因为接近尾声,所以更新得会有些“不积极”。

第八十讲 探索OI

=======================================

回复D直接查看目录

原文始发于微信公众号(

C/C++的编程教室

):第八十讲 探索OI

|

第七十九讲 细说map(5)

map我们说得似乎比vector还要多,我想我们应该加快步伐了,我们说了map的构造,说了map的排序方法,说了map的下标操作,ok,今天我们来看map的另一些操作:
———————————
template<typename Key,typename T,typename Cmp=less<Key>,

typename A=allocator<pair<const Key,T
>>>
class map{
public:

/////

// ///


iterator find(const key_type&

k);

//查找键为K的元素

const_iterator find(const key_type&

k) const;



size_type count(const key_type&

k);

//键为k的元素个数

iterator lower_bound(const key_type&

k);



const_iterator lower_bound(const key_type&

k)const;

//找到键为k的第一个元素。

iterator upper_bound(const key_type&

k);



const_iterator upper_bound(const key_type&

k) const;

//找到键大于k的第一个元素

pair<iterator,iterator>equal_range(const key_type&

k);



pair<const_iterator,const_iterator>equal_range(const key_type&

k) const;



pair<iterator,bool>insert(const valur_type&

val);

//插入元素

iterator insert(iterator pos,const value_type&

val);

//同上,不过pos完全没意义

template<typanema In>

void insert(In first,In last);

//插入一个序列

void erase(iterator pos);

//删除指定元素

size_type erase(const key_type&

k);

//删除键为k的元素

void erase(iterator first,iterator last);

//删除一个区间

void clear();

//删除所有元素

size_type size() const;

//返回元素个数

size_type max_size() const;

//和序列容器一样,直到内存用完

bool empty() const;

//是否为空

void swap(map&

);



/////
};


———————————————
上面的有些操作对map来说毫无意义,就比如说lower_bound和upper_bound,count,equal_rangge等等,为什么呢?因为map里面的键都是唯一的,所以这些东西操作对map来说毫无意义,但对于multimap来说又特别有用了,就比如:
———————————

void f(multimap<string,int>&

m){

multimap::iterator it = m.lower_bound("

abd"

);



multimap::iterator _it = m.upper_bound("

abd"

);



while(it != _it){

cout<<it->
first<<"

:"

<<it->second<<endl;


it++;


}
}
———————————-
上面打印出键为"

abc"

的所有元素,但是这些不会出现map中,都说了在map中毫无意义嘛,那么对于map,其他的我们都是可以使用的,比如:
——————————–

void f(map<string,int>&

m){

typedef map<string,int>::iterator CP;



CP it=m.find("

abc"

);



if(it != m.end()){ //说明已经找到了

cout<<it->
first<<"

:"

<<it->second<<end;


m.erase(it);


m.insert(make_pair("

abcd"

,99));


m.erase(m.find("

Chinese"

),m.find("

Math"

));


cout<<m.size()<<endl;


it = m.begin();


while(it != m.end()){

cout<<it->
first<<"

:"

<<it->second<<endl;


it++;


}

else

cout<<"

haven,t k=abc"

<<endl;


}
———————————–

Ok,关于map我们就说到这里了,如果有不懂的还是那句话直接问,接下来我们是该说些什么呢??????????关于C++我们已经说得差不多了,ok,这样吧,大家去思考一个问题,如果不使用iostream库,我们怎么让下面的代码运行:
—————————–
#include <stdlib.h>
int main(){

My_IO out;



MyInt a=5;



out<<a<<"

n"

;

//在控制台打印出5然后回车。

FILE* f(fopen("

Test.txt"

,"

w+"

));



My_IO fout(f);



fout<<a<<"

n"

;

//创建一个Test.txt文件,然后把5写进文本中;


return 0;


}
——————————

============================
回复D直接查看目录


原文始发于微信公众号(

C/C++的编程教室

):第七十九讲 细说map(5)

|

第七十八讲 细说map(4)

周末一晃就过,Ok,那就让我们继续我们的新内容吧,在上一讲,我们主要说map的下标操作和pair结构,同

时我们用一个例子来说明了map下标的实用,那么当我们需要添加学生成绩时,我们该怎么做呢?当然大家可能会

说,先把成绩储存进map<string,double>里面,然后再调用AddStudent不就可以了吗?当然这样做是没错的了,但是我们学了这么久的C++,难道就只能弄出个这玩意吗?C++最为显著的特点就是用类来表示概念,所以这一切操作我们都可以将他放进Students里面:
——————————-
class Students{
public:

/////

// ///

void AddScore(const string&

name,

const string&

course,

const double&

score);



////

// ////

};



void Students::AddScore(const string&

name,

const string&

course,

const double&

score){
_name[name].insert(make_pair(course,score));


}
——————————
如此一来,我们便可以这样使用:
—————————–
Students s;


map<string,double>m;


m["

Math"

] = 60;


m["

English"

] = 70;


m["

Chinese"

] = 80;


s.AddStudent("

ZhangSan"

,m);


s.AddStudent("

LiSi"

,m);


s.AddStudent("

WangEr"

,m);


cout<<"

====================="

<<endl;


s.ShowScore("

ZhangSan"

);

//打印ZhangSan成绩单
cout<<"

====================="

<<endl;


s.AddScore("

ZhangSan"

,"

Physics"

,78.5);

//为ZhangSan添加一门课程信息
ShowStudentsInfo(s);

//打印所有学生的信息
——————————-

第七十八讲  细说map(4)
Ok,这样一来,这个学生成绩的class算是差不多完成了,当然我们可以根据实际的需要再添加些信息,比如说,我们一开始就知道有哪些课程,我们可以把这些课程封装起来……好像扯得有些远了,这貌似不是我们现在说的主题。好吧,继续把头拉回来,我们来纵观一下这个学生成绩类,这个类几乎就是全靠map数据结构堆叠起来的,使用map,可以极大程度地减轻了我们的负担。就比如说上面的AddScore,如果我们不适用map的话,可能会绕些弯子,但是我们使用map的话,就一句话的事儿。insert是map里的一个操作方式,我们接下来会说,make_pair我们已经说过了,这个函数很有用,这不就见识到他的妙用了吗?

可能大家注意到一个现象了,就是我们打印出来的信息的顺序似乎和我们一开始所以为的有些区别,无论我们的插入顺序是什么,但打印顺序始终没变,这是为什么呢?今天,我们就来把这问题弄清楚,看是咋回事:
——————————
template<typename Key,typename T,typename Cmp=less<Key>,

typename A=allocator<pair<const Key,T
>>>
class map{
public:

/////

// ///


explicit map(const Cmp&

=Cmp(),const A&

=A());

//默认情况下,我们使用的是<操作符来比较的。

template<typename U>

map(U first,U last,const Cmp&

=Cmp(),const A&

=A());

//这里只有U是输入迭代器才能够通过。

map(const map&

);



typedef Cmp key_compare;


class value_compare:public binary_function<value_type,value_type,bool>{

friend class map;

//关于binary_function我们以后会说,这里大家只需要知道他是函数对象的一个基类就可以。

protected:

Cmp cmp;


value_compare(Cmp c):cmp(c){ }

public:

bool operator()(const value_type&

x,const value_type&

y) const{

return cmp(x.first,y.first);


}

};



key_compare key_comp() const;



value_compare value_comp() const;



// ///
};


—————————————-

从构造函数中,我们可以看出,Cmp已经给了默认值,所以,通常在我们不明确指出需要什么比较方式map会使用默认的小于操作符来做比较,所以,我们看到是一个字典序。

除了默认情况的用法,我们可以这样使用map:
————————————–
class Nocase:public binary_function<string,string,bool>{
public:

bool operator()(const string&

_str,const string&

str){

string::const_iterator it = _str.begin();


string::const_iterator _it = str.begin();


while (it != _str.end() &

&

_it != str.end()){

if (toupper(*it) != toupper(*_it))

return toupper(*it) <toupper(*_it) ? true : false;


++it;


++_it;


}

return _str.length() == str.length() ? false : _str.length() <str.length() ? true : false;


}
};

//这是一个函数对象,意思就是不区分大小写的对比,注意上面的三元操作符的嵌套使用。

int main(){

map<string, double>_m;


_m["

Math"

] = 80;


_m["

English"

] = 92;


_m["

Chinese"

] = 85;


_m["

abc"

] = 86;


map<string, double>::iterator _it = _m.begin();


while (_it != _m.end()){

cout <<_it->
first <<"

t"

<<_it->second <<endl;


_it++;




}

cout <<"

—————-华丽的分割线————-n"

;


map<string, double,Nocase>m;


m["

Math"

] = 80;


m["

English"

] = 92;


m["

Chinese"

] = 85;


m["

abc"

] = 86;


map<string, double,Nocase>::iterator it = m.begin();


while (it != m.end()){

cout <<it->
first <<"

t"

<<it->second <<endl;


it++;


}

return 0;


}
———————————————

第七十八讲  细说map(4)
当我们把我们自定义的一个比较器Nocase传递给map时,map里面的的函数对象value_compare()调用的就是Nocase(),也就是我们上面的函数对象。

好吧,现在已不早,今天就到这里吧,有什么不懂的直接提问。
=========================
回复D直接查看目录。


原文始发于微信公众号(

C/C++的编程教室

):第七十八讲 细说map(4)

|

第七十七讲 细说map(3)

上一讲我们说了泛化的复制构造函数,现在我们回来继续看看map,但是再说继续说map之前我觉得我们有必要先把pair弄清楚,其实也没什么好说的了,但是有一个函数我们是一定要知道的:
——————————–
template<typename T,typename U>
pair<T,U>make_pair(T&

t,U&

u){

return pair<T,U>(t,u);



}
———————————

不难发现,这是一个生成pair的函数,我们可以直接使用这个函数得到一个pair,而不必去指明pair类型。关于pair,我们说到这里就差不多了,他的存在多数是为了作为辅助来用,所以我们现在继续回来看看map,在上上讲里我们说了map里面的一些必要的typedef,所以现在我们来接着往下看关于map的一些操作:
———————————–
template<typename Key,typename T,typename Cmp=less<Key>,

typename A=allocator<pair<const Key,T
>>>
class map{
public:

/////

// ///


mapped_type&

operator[](const Key_type&

k);



/////
};


————————————-
这个操作符对现在的我们来说已经司空见惯了,不过不能否认这是一个极为好用的操作,因为我们通常都是用这个下标操作符来操作map的,比如:
————————————

void f(){

map<string, int>m;


m["

Math"

] = 80;


m["

English"

] = 92;


m["

Chinese"

] = 85;


map<string, int>::iterator it = m.begin();


while (it != m.end()){

cout <<it->
first <<"

t"

<<it->second <<endl;


it++;


}
}
———————————-

还记得以前给大家的一个练习吗?那是一位同学提出来的,就是怎么设计一个学生成绩的类,当时我把这个任务扔给大家,因为我想大家肯定有办法解决这个问题,现在我们再回去看这个问题,是不是发觉很简单了呢?如同上面一样,我们用一个map来储存成绩,然后再用map来存储学生姓名和成绩:
———————————–
map<string,map<string,double>>
———————————-

听起来有些复杂,确实是有些复杂的,不是正好我们说到map吗?好啊,反正就算是小试牛刀吧,我就简单的弄个出来看看吧:
———————————–
//Students.h
#ifndef _STUDENTS_H_
#define _STUDENTS_H_
#include <map>
#include <string>
using namespace std;


typedef map<string,double>Data_type;


typedef map<string,Data_type>MyType;


namespace My_Code{

class Students{

public:

Students();


~Students();


void AddStudent(const string&

name,Data_type&

data);


Data_type&

GetScore(const string&

name);


MyType&

GetName();


private:

MyType _name;


};


}
#endif
————————————

实现也很简单,仅作为参考:
————————————
//Students.cpp
#include "

Students.h"


using namespace My_Code;

Students::Students()
{


_name = MyType();


}

Students::~Students()
{
}


void Students::AddStudent(const string&

name,Data_type&

data){

_name[name] = data;


}

Data_type&

Students::GetScore(const string&

name){

return _name[name];


}

MyType&

Students::GetName(){

return _name;


}
———————————
然后,我们可以这样用:
———————————–
//TestStudents.cpp
#include <iomanip>
#include <iostream>
#include "

Students.h"


using namespace std;


using namespace My_Code;


int main()
{

Students s;



Data_Type m;



m["

Math"

] = 60;



m["

English"

] = 70;



m["

Chinese"

] = 80;



s.AddStudent("

ZhangSan"

,m);

//为了方便,所以我们都使用同一份成绩

s.AddStudent("

LiSi"

,m);



s.AddStudent("

WangEr"

,m);



MyType::iterator it = s.GetName().begin();



while(it != s.GetName().end()){ //遍历所有学生和打印出其成绩

cout<<it->
first<<"

:n"

;


Data_type::iterator _it = it->second.begin();


while(_it != it->second.end()){

cout<<setw(20)<<_it->
first<<"

: "

<<_it->second<<endl;


_it++;


}

it++;


}

_it = s.GetScore("

ZhangSan"

).begin();


cout<<"

ZhangSan:n"

;


while(_it != s.GetScore("

ZhangSan"

).end()){ //获取ZhangSan的成绩

cout<<setw(20)<<_it->
first<<"

: "

<<_it->second<<endl;


_it++;


}

return 0;


}
————————————–

这个类使用起来有些复杂不人性化,只要我们愿意,我们可以把写得更加好用一些,当然碍于篇幅的原因,这里我们就不多说了,希望曾经提问的同学还能够坚持下去,能够看到今天我们的实现方案,虽然不能拿出去商用,但可以作为参考,我坚持C++的原因就是这里:100个Java程序员写出的代码都是一种风格,而100个C++程序员写出来的程序就有100种风格。晕死,说了这么多,还是没说到重点,重点就是我们今天所说的operator[]的应用啦,我们使用map,很多情况下是通过operator[]来使用的。

今天好像说得不少了哦,算了吧,今天就先到这里吧,我们下周再见。


=================================
回复D直接查看目录


原文始发于微信公众号(

C/C++的编程教室

):第七十七讲 细说map(3)

|

第七十六讲 细说map(2)

我们继续来看map,不过在继续说map之前我们来看看pair的最后一个复制构造函数,这是一个模板构造函数,这个函数的意思其实很简单,就是可以让我们实现这样的转换:
———————————-
pair<int,int>p =f(8.8, 9.2);


cout <<p.first <<"

"

<<p.second <<endl;

//输出8和9
———————————-

像我们看到的一样,上面这个转换毫无意义,从double转换到int会造成数据丢失,所以这不是我们所希望的,那怎么办呢?在这里面本身就没啥办法,所以这里我觉得是C++不完美的地方,不知道java又或者C#会是怎样,还好这种情况我们都能够理解了的,如果这种情况出现在智能指针中呢?其实很久以前就想和大家来谈这个话题的了,但是一直都没机会,现在又遇到了,我们的C++马上就结束了,所以趁着现在和大家谈一谈。

现在我们来看一个问题:
—————————–
class B{…… };


class D:public B{…… };


class E:public D{…… };


B* p1 = new D;
B* p2 = new E;


const B* p3 = p1;


——————————-

通常情况下,我们像上面这样用没啥问题,而且运行得很好,当然,这就是内置指针的神奇之处,因为D和E 是B的派生,所以我们从D*或者是E*转换到B*,同样,我们还可以轻松的从非const转换到const,这看上去很好,而且通常我们也这样用,直到我们打算用对象来管理资源的时候……还记得我们以前写过一个智能指针吗
——————————–
template<typename T>
class SmPt{
public:

SmPt():point(new T){ }

SmPt(const T&

T0):point(new T(T0)){ }
////////////
……
……
////////////

};


———————————–
如果大家不记得这个智能指针的class,可以回头去看看我们的引用计数的那些章节吧,所以,如果我们打算用这个智能指针来管理我们资源的话,上面的情况就会变成下面这样:
———————————–
SmPt<B>p1 = SmPt<D>(new D);


SmPt<B>p2 = SmPt<E>(new E);


SmPt<const B>p3 = p1;


————————————

这看上去好像没问题,但是如果我们把模板展开,就知道有大问题了,首先我们来看第一句:SmPt<B>p1 = SmPt<D>(new D),这句代码展开后………………一个是B的具体化,一个却是D的具体化,原本B和D还算是有些关系的,但是现在是SmPt<B>和SmPt<D>,他们之间现在可以说是风马牛不相及,就像int和string一样,毫无任何关系,同样接下来的两句一样是毫无任何瓜葛,所以上面的代码是无法通过编译的,那么我们要怎么解决这个问题呢?这就是我们今天要说的话——写一个接受所有兼容类型的模板函数。

为了上面的代码能够完美的进行编译,所以我们只能写一个模板成员函数:
——————————————
SmPt(const SmPt<U>&

T0);


——————————————-
Ok,就这么简单,我们通过一个U构造一个T对象,上面的代码可以完美编译了,当然这不但可以完成我们想要的任务,而且比我想象的还要多,因为他还可以完成我们不想要的转换:
————————————–
SmPt<D>p = SmPt<B>(new B);


————————————–
这个转换是毫无意义,但是他却毫无征兆的通过编译,这可能会带来某种意想不到的bug,所以这种转换我们得避免,还记得我们SmPt里面有个get函数吗?
————————————–
T* get() const{

return point;

//point是T* point;

//如果不记得,还是可以回去看看引用计数那些章节的,里面有细说。
}
————————————–
有了这个函数,我们就可以避免那些毫无意义的转换了:
——————————————-
SmPt(const SmPt<U>&

T0):point(T0.get()){ }
——————————————-
现在只有能够通过隐式转换让U*转换成T*的情况下才能够通过编译。
Ok,今天就到这里吧,不过,最后还得说一下,模板复制构造函数或者模板赋值操作符并不是真正的我们平时所说的复制构造函数和赋值操作符,所以,当我们没有写赋值操作符或者是复制构造函数时,尽管我们写下模板赋值或者复制,编译器还是会为我们生成复制或者赋值操作符的。

==================================
回复D直接查看目录

原文始发于微信公众号(

C/C++的编程教室

):第七十六讲 细说map(2)

|

第七十五讲 细说map(1)

这些天都比较忙,所以没时间更新新内容,不过说实在的,对于C++的内容我们已经没多少可以拿出来说的了,当然对于基础知识,我们早就说完了,有些朋友,没有看过一讲,就问,有没有不要基础的?我想可能大概是我们的章节名都取得太随意太简单太直观化了些,所以有些朋友认为我们的内容说得很基础,但是我想真正看过里面内容的朋友肯定不会这么认为,我们里面引入的都是些很有技巧性的技术,好了,废话不多说,我们今天继续来说新的东西——关联容器。

关于容器,我们说了序列容器,至于大家吸收了多少这就是看大家的了,如果还有不明白的,可以回去多看几遍,其实,STL里面的东西不难如果我们只想去使用的话,尤其是在我们知道他们的原型之后,用起来更是轻松加愉快,当然,如果我们真心要研究STL的算法的话,那可能就有点难度了,所以,我只给大家提供原型和一些简单的实现,想要深入研究STL里面的东西,等我们打算去研究算法的时候再来研究吧。我们现在要做的就是拿来使用。

map在关联容器里的重要性如同vector在序列容器的重要性一样,所以,在说关联容器的时候我们肯定先来说说map。

map第一次接触他的时候我就想怎么就出了个地图了呢?后来又听说他叫字典,我就纳闷,怎么地图就变成了字典了呢?这个笑话有些冷,不过当初真是这样的。好了,我们今天先来看看map吧。
——————————–
template<typename Key,typename T,typename Cmp=less<Key>,

typename A=allocator<pair<const Key,T
>>>
class map{
public:

//还是老规矩,每个容器在一开始都要有一系列的类型定义

typedef Key key_value;



typedef T mapped_type;



typedef pair<const Key,T>value_type;

//这点和我们以前所说的序列容器有所不同,这也就是他的重点。

typedef Cmp key_compare;



typedef A allocator_type;



typedef typename A::reference reference;



typedef typename A::const_reference const_reference;



typedef implementation_defined1 iterator;



typedef implementation_defined2 const_iterator;



typedef typename A::size_type size_type;



typedef typename A::difference_type difference_type;



typedef std::reverse_iterator<iterator>reverse_iterator;



typedef std::reverse_iterator<const_iterator>const_reverse_iterator;



//

//

};


———————————

好吧,今天算是个开始,我们就先熟悉这一些的typedef,这些东西都没啥难的,不过我们要注意一点,那就是上面我们提到的value_type,这里他既不是T也不是Key,而是一个pair<Key,T>,在pair里面,Key是键,T是值,在pair里面,有两个成员,一个first,一个second,我们来看看pair是啥东西就知道了,还记得我们说的快速排序吗?当时我把微软的源码拿出来,里面就用到了pair。
————————–
template<typename Key,Typename T>
class pair{
public:

Key first;



T second;



pair():first(Key()),second(T()){ }

pair(const Key&

x,const T&

y):first(x),second(y){ }

template<typename U,typename V>

pair(const pair<U,V>&

p):first(p.first),second(p.second){ }
};


—————————

Ok,今天先到这里吧。

==========================

回复D直接查看目录

原文始发于微信公众号(

C/C++的编程教室

):第七十五讲 细说map(1)

|

第七十四讲 队列(3)

这一讲我们来探索队列,这是真正的队列,queue和priority_queue,在我们已经学会vector,list,deque和stack后再来看queue和priority_queue,他是这么的简单,简单得我们只需要给出的实现大家就会恍然明白——原来不过如此啊,好吧,我们先来看看queue的实现:
———————————
template<typename T,typename C=XXX<T>>
class queue{
public:

typedef typaname C::value_type value_type;



typedef typename C::size_type size_type;



typedef C container_type;

explicit queue(const C&

a = C()):c(a){ }

bool empty() const{

return c.empty();


}

size_type size() const{

return c.size();


}

value_type&

front()
{

return c.front();


}

const value_type&

front() const{

return c.front();


}

vaule&

back(const value_type&

x){

return c.back();


}

const value_type&

back() const{

return c.back();


}

void push(){

c.push_back();


}

void pop(){

c.pop_front();


}
protected:

C c;


};


———————————

同样,上面的XXX指的是容器,具有支持push_back()和pop_front()的容器,所以,他其实也就是某些容器的简单界面,但是由于我们的vector不支持pop_front,所以XXX就不可能会是vector了,但可以是list,也可以是deque,当然,默认情况下就是deque。queue的用途同样很多,windows的消息机制使用的就是queue,不过确切点说,应该priority_queue,如果是queue的话,那么可以这么来模拟消息循环:
———————————
struct Message{
///
};


void Server(queue<Message>&

q){

while(!q.empty()){

Message&

msg = q.front();


p.pop();


}
}
————————————-

使用太简单,所以大家可以自己尝试,下面我们再来看看priority_queue;
————————————-
template<typename T,typename C=XXX<T>,typename Cmp=Less<typename C::value_type>>
class priority_queue{
public:

typedef typaname C::value_type value_type;



typedef typename C::size_type size_type;



typedef C container_type;

explicit priority_queue(const Cmp&

ls=Cmp(),const C&

a = C()):c(a),cmp(ls){

make_heap(c.begin(),c.end(),cmp);


}

template<typename In>

priority_queue(In first,In last,const Cmp&

=Cmp(),const C&

=C());



bool empty() const{

return c.empty();


}

size_type size(){

return c.size();


}

const value_type&

top() const{

return c.front();


}

void push(const value_type&

);


void pop();


protected:

C c;



Cmp cmp;


};


——————————-

默认情况下,cmp使用的"

<"

操作符,所以top返回的总是最大的元素。
———————————
ps.push(5);


ps.push(1);


ps.push(8);


ps.push(9);


ps.push(2);


while (!ps.empty()){

cout <<ps.top() <<"

"

;


ps.pop();



}
———————————-
上面代码将会输出如下结果:
————————–
9 8 5 2 1
————————–

ok,队列算是说完了,有不懂的可以直接提问哈
=========================
回复D直接查看目录


原文始发于微信公众号(

C/C++的编程教室

):第七十四讲 队列(3)

|

第七十三讲 队列(2)

关于队列,我们说了deque,这一讲我们来看看堆栈stack,stack和deque一样简单,甚至还要简单一些,以至于我都不知道该从哪儿说起,好吧,我们来看看他的实现:
———————————
template<typename T,typename C=XXX<T>>
class stack{
public:

typedef typaname C::value_type value_type;



typedef typename C::size_type size_type;



typedef C container_type;

explicit stack(const C&

a = C()):c(a){ }

bool empty() const{

return c.empty();


}

size_type size() const{

return c.size();


}

value_type&

top()
{

return c.back();


}

const value_type&

top() const{

return c.back();


}

void push(const value_type&

x){

c.push_back();


}

void pop(){

c.pop_back();


}
protected:

C c;


};


———————————

上面的XXX指的是容器,具有支持push_back()和pop_back()的容器,所以,我们可以看得出,其实堆栈就是某些容器的界面,如果我们的XXX就是deque的话(默认情况下也就是我们上一讲说的deque),那么我们可以看下面的用法。
===================================

stack<char>s1;

//这句代码就是告诉编译器用deque<char>来保存char类型的元素。

stack<int,vector<int>>s2;

//这句代码就不再是默认情况了,我们使用的容器是vector,所以这里是用vector<int>来储存int类型的元素。
vector<int>v;


…………

void printf_element(v){

stack<int,vector<int>>s(v);


while(s.size()){

cout<<s.top()<<endl;


s.pop();


}

}
===================================
上面我们的例子是通过一个现有的容器v来初始化堆栈s的,不过这里有一个问题,当我们用一个现有的容器去初始化一个堆栈时,会复制里面的所有元素,这会是一笔不小的开销。还有一个问题:
—————————–
stack<int>s;


s.push(5);


if(s.empty()){

;


}else{

s.pop();

//1

s.pop();

//2
}
—————————
上面的代码看出什么了吗?在第一个pop时删除我们压入的5,但是第二个pop下去就出问题了,还记得我们以前说的下溢吗?就是一种无定义状态,自然会引起无数种bug。

好了,今天队列就说到这里吧,下一讲我们来结束队列——优先列队。
===============================
昨天回答一同学的问题,有朋友回复说关于中文怎么判断,因为昨天下班回来太晚,所以也没验证,今天再看了下昨天的回答,发现还是有些问题的,因为中文不同于一般的英文字符,一般的英文字符可以是一个字节,但是中文不是一个字符,所以完整的判断方式应该如下:
——————————
int myStr::ChineseChar(string str){
mstr = str;


count1 = 0;



for (int i = 0;

i <str.length();

i++){

wchar_t ch = mstr.at(i);



if (int(ch)>127){

i++;


wchar_t
temp = str.at(i);


if (int(temp) >127)

++count1;


}

}

return count1;


}
————————-


如果我们不用wchar_t而用char的话,在对比ASCII的时候用他和0对比:
————————–

char ch = mstr.at(i);



if(int(ch) <0){

i++;


char
temp = mstr.at(i);


if(int(temp)<0)

++count1;


}
————————-

其实通常情况下我们想要比较ASCII,可以不用转换为int的,因为char对象就可以完成隐士转换,但是为了明显一些,我通常都会显示转换。
==============================
回复D查看目录


原文始发于微信公众号(

C/C++的编程教室

):第七十三讲 队列(2)

|

第七十二讲 队列(1)


关于容器,我们说了最为重要的两个,vector和list,现在STL里面剩下的也不多了,现在只剩下一个叫做队列的东西,不过队列的东西有三个:deque,stack,queue,当然还有一个priority_queue。

今天我们主要说的是deque,其实关于deque,我们实在没啥好说的,因为如果我们要再来演示一遍,那么等于又重复了我们vector和list一遍。

deque是一种双端列队,他是一门优化过的序列,他不但拥有vector的操作,还拥有list的前端操作,但是对于其他地方的插入和删除和vector一样效率低得吓人,现在我们已经知道deque的样貌,而且他所拥有的方法我们已经都见识过,所以,现在学习他唯一的办法就是来看一个实例。

——————————-

template<typename T>


void show(deque<T>&

ml){

deque<T>::iterator it = ml.begin();

while (it != ml.end())

cout <<*it++ <<"

"

;

cout <<endl;

}

int main(){


int a[] ={ 2, 3, 6, 1, 4, 3, 6, 6, 4, 3, 2, 7, 9, 2
};

deque<int>mDeque;


for (int i = 0;

i <7;

i++)

mDeque.push_back(a[i]);

show(mDeque);


for (int i = 0;

i <7;

i++)

mDeque.push_front(a[7 + i]);

show(mDeque);


return 0;

}

—————————-

运行程序得到结果如下:

————————–

2 1 6 3 4 3 6

2 9 7 2 3 4 6 2 1 6 3 4 3 6

—————————

好吧,大家可能会问,为什么deque这么好用,为什么还要用vector呢?当然还是因为效率问题,deque是优化过的容器,所以在效率上面还是赶不上vector,尤其是使用前端操作的时候,效率更会大打折扣,而当他们需要插入或者是删除元素的时候,我们的选择当然会是list,对于很大的数据来说,插入或者删除中间的数据要比把这个容器复制给list,然后再来删除或者插入,当然这些东西还得视情况而定。

OK,今天就这样吧,我们下周再见。


================================

回复D直接查看目录

原文始发于微信公众号(

C/C++的编程教室

):第七十二讲 队列(1)

|