九天雁翎的博客
如果你想在软件业获得成功,就使用你知道的最强大的语言,用它解决你知道的最难的问题,并且等待竞争对手的经理做出自甘平庸的选择。 -- Paul Graham

浅谈C++类(1)--概念和构造函数

欢迎转载,但请标明作者 “九天雁翎”,当然,你给出这个帖子的链接更好。

类多么重要我就不多说了,只讲讲学习,因为个人认为类的学习无论从概念的理解还是实际代码的编写相对其他C兼容向的代码都是比较有难度的, 对于以前学C 的人来说这才是真正的新概念和内容,STL其实还比较好理解,不就是一个更大的函数库和代码可以使用嘛。虽然vector,string就是类,不过我们却不需要这样去理解他们,就可以很好的使用了。

先说明,1,这是非常初级的东西。2,你懂了就不需要看了。3,我写出来是帮助还不懂得人。4,我自己也还不太懂,所以才写下来,梳理一下,希望自己能更好的理解,因为我相信一句话,很好的理解一个东西的好方法是把这个东西教给别人。有什么不对的地方,欢迎指出,我非常感谢,还有很多时候,某种方法是不允许的,了解也很重要,但我不想给出错误的例子,那样很容易给出误导,只讲这样是错误的,希望你可以自己输入去尝试一下,看看得出的是什么错误。

一、概念:就Bjarne Stroustup自己说,来自于Simula的概念(The Design and Evolution of C++),我不懂Simula,所以,还是对我没有什么帮助,基本上,都说类是具体对象(实例)的抽象,怎么抽象?就是把一个实例的特征拿出来,比如,水果是一个类,苹果就是一个实例,苹果有水果的特征。我们只要从苹果香蕉中把特征抽象出来“class Fruits{ }”;就好了。然后 “Fruits apple”,表示苹果是一个水果。就像人是一个类的话,我们就都是实例。一下也讲不清,不过也可以从另一个角度去理解,就是Bjarne Stroustup自己说的,一个Class其实就是一个用户定义的新Type,这点上和Struct没有什么本质上的区别,只是使用上的区别而已。之所以没有把它直接叫作Type是因为他的一个不定义新名字的原则。

二、使用:我一直觉得比较恼火,光看概念是没有用的,学习程序,自己编写代码是最快的。下面是几个步骤:

1:最简单的一个类。

C++中使用任何东西都要先定义吧,类也不例外。用水果举例,水果的特征最起码的名字先这1个吧。名字用string表示。

例1.0:

#include <string>
#include <iostream>
using namespace std;
class Fruit               //定义一个类,名字叫Fruit
{
public:             //标号,表示这个类成员可以在外部访问
    string name;           
};
int main()
{
    Fruit apple = {"apple"};  //定义一个Fruit类对象apple
    cout<< apple.name<<endl;  //使用apple的成员name
    return 0;
}

在这里说明,以后其他细节我都省略说明了,比如#include,using,cout等等,先去学会吧。我只说类;你会发现其实在这里把class换成struct没有任何问题,的确,而且换成sturct后”public:” 标号都可以省略,记住,在C++里面,struct与class其实没有本质的区别,只是stuct默认成员为public而class默认为private。public顾名思义,就是公共的,谁都可以访问,private自然就是私人的,别人就不能访问了,你把例1.0的public:标号这行去掉试试。你会得到两个错误,1,不能通过 Fruit apple = {“apple”};形式定义,2,cout«行不能访问私有成员。这里class几乎就和c里面的struct使用没有区别,包括apple.name点操作符表示使用对象apple里面的一个成员,还有Fruit apple = {“apple”};这样的定义初始化方法。很好理解吧,不多说了。说点不同的,C++里面class(struct)不仅可以有数据成员,也可以有函数成员。比如,我们希望类Fruit可以自己输出它的名字,而不是我们从外部访问成员。

例1.1:

#include <string>
#include <iostream>
using namespace std;
class Fruit               //定义一个类,名字叫Fruit
{
public:             //标号,表示这个类成员可以在外部访问
    string name;            //定义一个name成员           
    void print()              //定义一个输出名字的成员print()
    {
      cout<< name<<endl;
    }
};
int main()
{
    Fruit apple = {"apple"};  //定义一个Fruit类对象apple
    apple.print();  //使用apple的成员print
    return 0;
}

这里你会发现与C的不同,而这看起来一点点地不同,即可以在class(struct)中添加函数成员,让C++有了面向对象特征,而C只能是结构化编程(这在C刚出来的时候也是先进的代表,不过却不代表现在的先进编程方法)。还有,你发现定义函数成员和定义普通函数语法是一样的,使用上和普通成员使用也一样。再进一步,在C++中有构造函数的概念,先看例子

例1.2:

#include <string>
#include <iostream>
using namespace std;
class Fruit               //定义一个类,名字叫Fruit
{
public:             //标号,表示这个类成员可以在外部访问
    string name;            //定义一个name成员           
    void print()              //定义一个输出名字的成员print()
    {
      cout<< name<<endl;
    }
    Fruit(const string &st)      //定义一个函数名等于类名的函数成员
    {
      name = st;
    }

};
int main()
{
    Fruit apple = Fruit("apple");  //定义一个Fruit类对象apple
    Fruit orange("orange");
    apple.print();  //使用apple的成员print
    orange.print();     
    return 0;
}

例子1.2里面的函数名等于类名的函数成员就叫作构造函数,在每次你定义一个新对象的时候,程序自动调用,这里,定义了2个对象,一个apple, 一个orange,分别用了2种不同的方法,你会发现构造函数的作用,这里,要说的是,假如你还按以前的方法Fruit apple = {“apple”}定义apple你会编译失败,因为有了构造函数了,Fruit apple就定义成功了一个对象,让apple对象等于{“apple”}的使用是不允许的。对象只能等于对象,所以你可以先用Fruit(“apple”)构造一个临时的对象,然后让apple等于它。orange对象的定义就更好理解了,直接调用构造函数嘛。这里要说的是,你不可以直接Fruit banana了,因为没有可以用的构造函数,而没有用构造函数前,你是可以这样做的。直接Fruit apple,再使apple.name = “apple”,是完全可以的。

例1.3:

#include <string>
#include <iostream>
using namespace std;
class Fruit               //定义一个类,名字叫Fruit
{
public:             //标号,表示这个类成员可以在外部访问
    string name;            //定义一个name成员           
    void print()              //定义一个输出名字的成员print()
    {
      cout<< name<<endl;
    }

};
int main()
{
    Fruit apple;  //定义一个Fruit类对象apple
    apple.name ="apple"; //这时候才初始化apple的成员name
    apple.print();  //使用apple的成员print
     
    return 0;
}

而有了构造函数以后就不能这样了,怎么样不失去这种灵活性呢?你有两种办法。其一是重载一个空的构造函数,记得,构造函数也是一个函数,自然也可以重载罗。你还不知道什么是重载?那先去学这个简单的东西吧,类比那家伙复杂太多了。

例1.4:

#include <string>
#include <iostream>
using namespace std;
class Fruit               //定义一个类,名字叫Fruit
{
public:             //标号,表示这个类成员可以在外部访问
    string name;            //定义一个name成员           
    void print()              //定义一个输出名字的成员print()
    {
      cout<< name<<endl;
    }
    Fruit(const string &st)
    {
      name = st;
    }
    Fruit(){}    //重载一个空构造函数
};
int main()
{
    Fruit apple;  //定义一个Fruit类对象apple,这时是允许的了,自动调用第2个构造函数
    apple.name ="apple"; //这时候才初始化apple的成员name
    apple.print();  //使用apple的成员print
     
    return 0;
}

第二种办法,就是使用构造函数默认实参;

例1.5

#include <string>
#include <iostream>
using namespace std;
class Fruit               //定义一个类,名字叫Fruit
{
public:             //标号,表示这个类成员可以在外部访问
    string name;            //定义一个name成员           
    void print()              //定义一个输出名字的成员print()
    {
      cout<< name<<endl;
    }
    Fruit(const string &st = "banana")
    {
      name = st;
    }
};
int main()
{
    Fruit apple;  //定义一个Fruit类对象apple
    apple.print();
    apple.name ="apple";  //这时候才初始化apple的成员name
    apple.print();  //使用apple的成员print
     
    return 0;
}

这个程序里面,当你直接定义一个无初始化值的apple对象时,你发现,他直接把name表示为banana。也许现在你会问,为什么需要构造函数呢?这里解释以前留下来的问题。即不推介使用Fruit apple = {“apple”}的原因。因为这样初始化,你必须要保证成员可以访问,当name为私有的时候,这样可就不奏效了,为什么需要私有呢?这就牵涉到类的数据封装问题,类有不希望别人访问的成员,以防破坏内部的完整性,也以防误操作。这点上要讲就很复杂了,不多讲了。只讲操作吧。

例1.6

#include <string>
#include <iostream>
using namespace std;
class Fruit               //定义一个类,名字叫Fruit
{
               //没有标号了,表示这个类成员不可以在外部访问,class默认为private哦
    string name;            //定义一个name私有成员           
public:  
    void print()              //定义一个输出名字的成员print()
    {
      cout<< name<<endl;
    }
    Fruit(const string &st = "banana")
    {
      name = st;
    }
};
int main()
{
    Fruit banana;  //定义一个Fruit类对象

    banana.print();
    // banana.name ="apple";  //这时候才改变banana的成员name已经是不允许的了
    // 你要定义一个name等于apple的成员必须这样:
    Fruit apple("apple");
    apple.print();
     
    return 0;
}

要说明的是,构造函数你必须定义成公用的啊,因为你必须要在外部调用啊。现在讲讲构造函数特有的形式,初始化列表,这点和一般的函数不一样。

例1.7:

#include <string>
#include <iostream>
using namespace std;
class Fruit               //定义一个类,名字叫Fruit
{
  string name;            //定义一个name成员           
public:  
  void print()              //定义一个输出名字的成员print()
  {
    cout<< name<<endl;
  }
  Fruit(const string &st = "banana"):name(st){}  //看到不同了吗?
};
int main()
{
  Fruit banana;  //定义一个Fruit类对象

  banana.print();
   
  return 0;
}

在参数表后,函数实体前,以“:”开头,列出的一个列表,叫初始化列表,这里初始化列表的作用和以前的例子完全一样,就是用st初始化name,问题是,为什么要特别定义这个东西呢?C++ Primer的作者Lippman在书里面声称这时许多相当有经验的C++程序员都没有掌握的一个特性,因为很多时候根本就不需要,用我们以前的形式就够了但有种情况是例外。在说明前我们为我们的Fruit加个固定新成员,而且定义后不希望再改变了,比如颜色。

例1.8:

#include <string>
#include <iostream>
using namespace std;
class Fruit               //定义一个类,名字叫Fruit
{
  string name;     //定义一个name成员           
  const string colour;
public:  
  void print()              //定义一个输出名字的成员print()
  {
    cout<<colour<<" "<<name<<endl;
  }
  Fruit(const string &nst = "apple",const string &cst = "green"):name(nst),colour(cst){}  
};
int main()
{
  Fruit apple;  //定义一个Fruit类对象apple
  apple.print();
   
  return 0;
}

在这里你把colour的初始化放到{}里面,用以前的那种方法,你会发现编译错误,因为它是const的,而实际上放在{}里面是个计算阶段,而放在初始化列表里面就可以,因为初始化列表的使用是在数据定义的时候就自动调用了,因为这个原因,数据的调用顺序和初始化列表里面的顺序无关,只和数据定义的顺序有关,给两个例子,比如你在上面的例子中把初始化列表改为”:colour(name),name(nst)”没有任何问题,因为在定义colour前面,name 就已经定义了,但是”:name(colour),colour(cst)”却不行,因为在name定义的时候colour还没有被定义,而且问题的严重性在于我可以通过编译………太严重了,所以在C++ Primer不推荐你使用数据成员初始化另外一个数据,有需要的话,可以”:name(cst),colour(cst)”,一样的效果。另外,初始化列表在定义时就自动调用了,所以在构造函数{}之前使用,你可以看看这个例子:

例1.9 :

#include <string>
#include <iostream>
using namespace std;
class Fruit               //定义一个类,名字叫Fruit
{
  string name;     //定义一个name成员           
  const string colour;
public:  
  void print()              //定义一个输出名字的成员print()
  {
    cout<<colour<<" "<<name<<endl;
  }
  Fruit(const string &nst = "apple",const string &cst = "green"):name(nst),colour(cst)
  {
    name +="s";    //这时name已经等于"apple“了
  }   
};
int main()
{
  Fruit apple("apple","red");  //定义一个Fruit类对象apple
  apple.print();
   
  return 0;
}

最后输出red apples。先讲到这里吧,你明白一点什么是类没有?像我一样学了老半天还不明白的,坚持住,多练习,总能明白的。我现在似乎明白的多一点了:)

阅读全文....

关于容器输出的学习与简化过程

在学习C++标准库的过程中,各种容器是很大一块,每次验证自己的结果输出容器的时候除了string 可以简单的cout«输出以外,其他的都要for()循环用迭代器遍历,比如输出vector vec容器就要这样

for(vector<int>::iterator it = vec.begin(); it != vec.end(),++it)
    cout<<*it<<" :;
cout<<endl;

很不方便,所以以前就自己编了个重载函数printCon以输出常用的容器

void printCon(list<int>::const_iterator,list<int>::const_iterator);  
void printCon(deque<int>::const_iterator,deque<int>::const_iterator);  
void printCon(vector<int>::const_iterator,vector<int>::const_iterator);  
void printCon(vector<string>::const_iterator,vector<string>::const_iterator);

main()

{return 0;}
void printCon(list<int>::const_iterator first,list<int>::const_iterator last)  
{  
  cout<<endl;  
  for(;first != last;++first)  
  {  
    cout <<*first<<" ";  
  }  
  cout<<endl;  
}  
void printCon(deque<int>::const_iterator first,deque<int>::const_iterator last)  
{  
  cout<<endl;  
  for(;first != last;++first)  
  {  
    cout <<*first<<" ";  
  }  
  cout<<endl;  
}  
void printCon(vector<int>::const_iterator first,vector<int>::const_iterator last)  
{  
  cout<<endl;  
  for(;first != last;++first)  
  {  
    cout <<*first<<" ";  
  }  
  cout<<endl;  
}  
void printCon(vector<string>::const_iterator first,vector<string>::const_iterator last)  
{  
  cout<<endl;  
  for(;first != last;++first)  
  {  
    cout <<*first<<" ";  
  }  
  cout<<endl;  
}

使用起来还算方便,也简洁,只要两个迭代器就可以遍历输出容器,而且输出范围内的容器也可以。就是代码比较复杂,假如要适应全部的容器,代码将会复杂的吓人,但我没有学过模版,不知道那样是不是可以使这个函数简单一些,不过学了流迭代器以后,问题得到了解决,上面那个问题,只需要

std::ostream_iterator<int> ost_iter(cout," ");  
std::copy(vec.begin(),.vec.end(),ost_iter);

就可以得到解决了,真是简单多了啊,终于知道为什么C++要定义输出流迭代器这乍一看没有什么用的东西了。

阅读全文....

关于C++标准库泛型算法reverse的学习笔记

C++ Primer中这样描述reverse 反向排列元素

一个容器为 9,8,7,6,5,4,3,2,1,0,sort后为0,1,2,3,4,5,6,7,8,9。

一个容器为0,1,2,3,4,5,6,7,8,9,sort后还为0,1,2,3,4,5,6,7,8,9。

假设一个容器为0,1,2,3,4,5,6,7,8,9你认为reverse以后为什么呢?没有错,9,8,7,6,5,4,3,2,1,0。

但是一个容器本来就为 9,8,7,6,5,4,3,2,1,0呢?还是9,8,7,6,5,4,3,2,1,0吗?他们不是本来就降序排列了吗?

我以前就是这样理解的,不过实际使用才知道reverse不时排序算法,仅仅是反向排列。9,8,7,6,5,4,3,2,1,0

reverse后变成0,1,2,3,4,5,6,7,8,9。要得到降序排列的方法,好像可以先sort,后reverse.不知道我说的对不对。

具体的验证代码就很简单,不列出来了。

阅读全文....

关于C++标准库泛型算法merge的学习笔记

#include <vector>
#include <string>
#include <iostream>
#include <list>
#include <algorithm>
#include <iterator>
using namespace std;

int main(int argc, char *argv[])
{
    std::ostream_iterator<int> ost_iter(cout," ");
    list<int> lst1;
    for(int i = 0;i<10;++i)
        lst1.push_back(i);
    list<int> lst2;
    for(int i = 0;i<10;++i)
        lst2.push_front(i);
    merge(lst1.begin(),lst1.end(),lst2.begin(),lst2.end(),ost_iter);
    cout<<endl;
    return 0;
}

会发现程序编译成功,但是运行出错,原因在于merge算法应用时必须先排序才行,改成下面这样:

。。。省略预编译与using

int main(int argc, char *argv[])
{
    std::ostream_iterator<int> ost_iter(cout," ");
    list<int> lst1;
    for(int i = 0;i<10;++i)
        lst1.push_back(i);
    list<int> lst2;
    for(int i = 0;i<10;++i)
        lst2.push_front(i);
    std::copy(lst1.begin(),lst1.end(),ost_iter);
    cout<<endl;
    std::copy(lst2.begin(),lst2.end(),ost_iter);
    cout<<endl;
    lst1.sort();
    std::copy(lst1.begin(),lst1.end(),ost_iter);
    cout<<endl;
    lst2.sort();
    std::copy(lst2.begin(),lst2.end(),ost_iter);
    cout<<endl;
    merge(lst1.begin(),lst1.end(),lst2.begin(),lst2.end(),ost_iter);
    cout<<endl;
    return 0;
}

文件成功,但是两个容器0,1,2,3,4,5,6,7,8,9合并,竟然变成0,0,1,1,2,2。。。。。奇怪吧。另外,lst1.sort()可以去掉,因为它已经是排好序的了,换句话说,merge运行正确的前提是排好序,而不是一定要你先运行sort()。另外merge不仅有合并的意思,还有融入的意思,感觉这里更像是融入,而不是简单的合并,即我理解的合并在后面。当然,假如仅仅是加在后面用insert就可以了,没有必要用merge 而且,假如merge像,那样还需要先排序干什么呢?

阅读全文....

C++ Primer 中文版 第4版 习题10.2

/*我在这里写下部分C++ Primer 中文版 第4版 习题的个人解答和看法(注:我没有买答案书,所以不保证正确,你觉得错的,希望你能告诉我)源代码运行的要求和书上一样,省略了预编译和using行。假如有什么说的不详细,你还不懂,可以问我,不过我也是初学者,不一定知道阿,看我还在学C++ Prime 就知道我是初学者了。欢迎转载,但是请保留作者名“九天雁翎”。*/

typedef pair<string,int> sipair;    //简化定义方法  
typedef vector<sipair> sidv;  

int main()  
{  
    string stra;  
    int ia;  
    sipair paira,pairb;  
    sidv sidva;  

    while(cin>>stra>>ia)  
    {  
        paira.first = stra;  
        paira.second = ia;    //第一种方法  
        pairb = make_pair(stra,ia);  //第二种方法  
        sipair pairc(paira);            
        sipair paird(stra,ia);          //不知道上面这两种算不算,原题要求3种  
        //我认为这几种方式的理解都还好,只是使用上的简单有点差别  
        sidva.push_back(paira);  
    }  

    for(sidv::iterator it = sidva.begin();it != sidva.end();++it)  
    {  
        cout<<"the pair is:"  
            <<it->first<<" "<<it->second<<endl;  
    }  
      
    return 0;  
}

阅读全文....