3、C++快速入门

参考书籍:

C++程序设计教程_第二版_钱能    //篇幅较少,适合快速学习

C++ Primer Plus  第六版  中文版   //篇幅较大,讲的非常详细

C++一般必须包含的头文件是#include <iostream>;导入的命名空间是:using namespace std;

1、访问控制,类和对象

class是对struct的扩展,含有数据成员和成员函数

访问控制:private、public、protected,其中private声明的数据成员仅供类内部函数使用,public声明的类外部程序也可使用,默认的属性是private

int a:int是类型,a是变量

Person per :Person是类,per是对象

(在类中通过”this->数据成员”表示类内成员)

 

2、程序结构

  类定义(.h)/类实现(.cpp)

  如果在类中仅声明了函数,在类外怎么实现?void 类名::函数名(参数){函数体}

  A类实现Person类,其提供.h和.cpp,在A.h中仅声明类,其中类成员函数也仅声明,在A.cpp中通过类名::函数名来实现成员函数;B类实现main,B不关心Person类怎么实现,只需要#include <person.h>

  

  命名空间

  如果main文件中include多个h文件,同时这些h文件中存在同名的函数,其返回值和参数都一样,这个时候在h和其对于的cpp中把全部或者部分同名的函数使用namespace 名字A{}扩起来,在main中通过“名字A::同名函数名”还区分

eg:Person.h

 #include <stdio.h>

  namespace A{

  class Person{

  private:

    char *name;

    int age;

    char *work;

  public:

    void setName(char *name);

    int setAge(int age);

  }

  void printVersion(void);

  }

Person.cpp

  #include <stdio.h>

  #include "person.h"

  namespace A{

  void Person::setName(char *name)

  {

    this->name = name;

  }

  int Person::setAge(int age)

  {

    this->age = age;

    return 0;

  }

  void printVersion(void)

  {

    printf("Person111111");

  }

  }

Main.cpp

  #include <stdio.h>

  #incldue "person.h"

  using A::Person;//加上这句后,main中类的声明前面可以不加“命名空间::”,这句话把A::Person放入global namespace,以后可以使用Person来表示A::Person

  //using namespace A;作用同是把A空间全部导入,注意在导入多个命名空间的时候,如果命名空间中存在同名同参函数,在使用的时候还是要加上“命名空间::”来区分,但在导入的时候不会出现问题

  int main(int argc,char **argv)

  {

    A::Person per;//声明命名空间后,类的声明需要在前面加上“命名空间::”

    A::printVersion();

    return 0;

  }

 

3、重载、指针和引用

 重载:函数名系统,参数不同(类型、数量、顺序)

 指针和引用:引用就是别名,引用定义时必须初始化,且其只能引用变量,不能是常量:int a;int &b = a//b就是a的引用,b所指的内存和a一样;C++中经常使用引用来传递参数,引用传递的是地址,仅四字节,否则如果参数是对象,则传递对象耗费空间大

 引用举例:

 int add(int &b)

  {

    b = b +1;

    return b;

  }

  调用add:

  int a = 99;

  add(a);

  cout<<a<<endl;//引用会导致a变量被修改

 

4、构造函数   

  声明变量时调用构造函数

  eg:Person per(变量1,变量2);//会调用构造函数void  Person(变量1,变量2){};

  注意:调用默认构造函数是通过Person per,而不是Person per();其会被理解为一个函数声明,返回值是Person

  Person *per4 = new  Person;

  Person *per5 = new  Person();

  Person *per6 = new  Person[2];

  Person *per7 = new  Person("list",18,"student");

  Person *per8 = new  Person("list",18);

  如果在构造函数中通过new分配了空间,应该在析构函数中delete掉,否则只能等到主函数推出后才能被回收,析构函数在实例化对象被销毁之前的瞬间被调用,比如在一个函数中会声明这个实例化对象,在函数执行完退出时销毁该实例化对象。但是如果在函数中是通过new来实例化一个对象的时候(Person *per4 = new  Person;),函数退出时per4 指针所指向的对象不会被销毁,其析构函数不会被调用,只能通过delete per4来销毁或者等整个main程序退出的时候才能回收空间,但次数不会调用对象的析构函数。

  eg:PersonPerson(){

      this->name = new char[10];

    }

    ~PersonPerson(){

      if(this->name)

        delete this->name;

    }

 

  对象有默认的无参构造函数、无参析构函数、还有一个默认的拷贝构造函数,即在声明一个实例化对象的时候提供的参数是对象:Person per("zhangsan",18);Person per2(per);即per2使用per来初始化的,这个时候调用的构造函数就是默认的拷贝构造函数,per2的属性内容和per一样,共享地址空间,如果per内存被释放,per2在执行释放的时候也会再次释放该内存,存在风险,所以需要提供自己的拷贝构造函数:

eg:Person(Person &per)

  this->age = per.age;

  this->name = new char[strlen(per.name)+1];

  strcpy(this->name,per.name);

  函数中定义的静态实例化对象在函数退出的时候不会被销毁;再次进入函数的时候也不会被创建,其还存在

  全局对象、main中局部对象、子函数中局部对象中的构造函数执行顺序:1、全局对象构造函数->main中局部对象构造函数->子函数中局部对象构造函数

  如果在类A中声明了成员类对象B,则在实例化A对象的时候先调用B的构造函数,在调用A的构造函数;析构函数是先调用A的析构,在调用B的析构,即析构函数的调用顺序与构造函数的调用顺序相反(不管A中有多少个对象)

  如果类中定义了有参构造函数,则系统不会在提供无参的构造函数,这时候在声明无参对象时会出错,因为已经没有无参构造函数了

 

5、静态成员和友员函数

  static修饰的成员属于类,不属于对象,其仅有一份,通过“类名::成员名”访问,并且私有的static成员仅能被static函数访问

  并且static成员必须在类外面定义和初始化:int Person::cnt =0;在类中仅是声明,在类外给其分配空间和初始化

 

  被类设置为的友员函数可以访问本类的私有成员:eg:在类中通过“friend 函数声明”来声明该类的友员函数

 

6、操作符重载-通过类外函数实现

  通过操作符重载可以实现通过“+”、“-”等符号实现对象的加减

  class Point{

  private:

      int x;

      int y;

  friend Point operator+(Point &p1,Point &p2);

  }

  Point operator+(Point &p1,Point &p2)

  {

    Point n;

    n.x = p1.x + p2.x;

    n.y = p1.y + p2.y;

    return n;

  }

  int main(int argc,char **argv)

  {

    Point p1(1,2);

    Pont p2(2,3);

    Point sum = p1+p2

  }

  来实现Point p(1,2);p++;++p;

  注意:下面两个需要在类中声明为友员,目的是为了访问类的私有成员,如果成员是公有的,则不需要声明为friend;

  Point operator++(Point &p)//++p,这里的重载函数返回了个Point对象,返回的时候会根据p值来调用构造函数Point(const Point &p)来构造一个对象,如果不使用这个返回的对象,立马会调用其析构函数并且被销毁;这样一个构造和析构过程比较浪费资源,如果把返回值声明为Point& operator++(Point &p),这样就不会新声明一个对象了,直接返回传入的p的引用,但需要注意返回引用和返回值的时候不能影响程序的执行结果

  {

 

    p.x += 1;

    p.y += 1;

    return p;

  }

  Point operator++(Point &p,int a)//p++,在main中也可以通过operator++(p,0)来调用

  {

    Point n;

    n=p

    p.x += 1;

    p.y += 1;

    return n;

  }

  使用的时候p++;++p就可以

  

  对cout的重载,eg:Point m,n;cout<<m<<n;//cout<<m返回的就是cout,是对cout的引用这样才能cout<n;

  ostream& operator<<(ostream &o,Point p)//这个函数也可以在main中通过“operator<<(cout,p)”调用

  {

    cout<<"("<<p.x<<","<<p.y<<")";//<<endl这里的endl表示回车

    return o;

  }

 

7、操作符重载-通过类内函数实现

  把上面6中这些外部重载的函数移到类内部就可以,通过减少参数,因为类内部函数在被对象调用的时候,这个对象就是一个参数eg:p1.operator+(p2)

  现在执行m = p1+p2表示为m=p1.operator+(p1);

  注意:cout输出不能在类内部实现,因为"p.operator<<"其第一个参数是Point类型,而函数的第一个参数数ostream类型

  重载“=”:Person& operator=(const Person& p)//如果不重载“=”,那么p = p1,会是的p里面的变量会和p1里面的变量指向同一片内存(注意:Point p=p1执行的是拷贝构造函数)

      {

        if(this == &p)

          return *this;

        this->age = p.age;

        if(this->name){

          delete this->name;

        }

        if(this->work){

          delete this->work;

        }

        this->name = new char[strlen(per.name)+1];

        strcpy(this->name,per.name);

        this->work = new char[strlen(per.work)+1];

        strcpy(this->work,per.work);

        return *this;

      }

 

8、访问控制和继承

  class Person{};

  calss Student:public Person{};//Student类继承Person类

  基类成员在派生类中的访问控制属性

       基类访问属性  public                protected                 private

  继承类型

  public          public           protected      隔离

  protected        protected       protected       隔离

  private          private        private       隔离

  (无论那种继承方式,在派生类内部使用父类时并无发别;仅影响外部代码对派生类的使用和派生类的之类)

  说明:1、派生类不能访问基类的私有成员;

     2、派生类可以通过protected或者public的成员函数访问私有成员;

       3、派生类可以访问protected成员,其他外部代码不可以(protected成员外界不可访问);

     4、派生类继承到的成员可以修改成员的权限(protected可以修改为public、private,但是private成员不能被修改权限)

      (eg:在派生类中通过“public:

                    using 父类名::父类成员名或者函数名”可以修改成员变量和函数的权限)

    Base &b2=d1;// 子类对象当父类对象

   b2.print(); // 调用父类函数

 

9、多重继承

  派生类(子类)有多个父类(基类)

  eg:

  class Sofa{};

  class Bed{};

  class Sofabed:public Sofa,public Bed{};//这里如果不写继承方法,则默认的是private继承

  

  虚拟继承:D继承A和C,这个时候在D中怎么访问A和C同名的函数呢:1、d.A::同名函数();2、把A和C中同名的部分抽象出来一个类B,A和C通过virtual来虚继承,对于虚继承的A和C,在子类D中只会有一份B中的成员(class A:virual public B)

 

10、构造顺序

  A、先父类(基类)后子类(派生类)

  B、对于父类:先虚拟基类后一般基类

  C、对于子类:先对象成员,后自己的构造函数

 

11、多态(使用相同的调用方法,对于不同的对象会调用不同的类里面的实现的函数,根据传入的对象类型自己识别该类型,并调用类型的函数)

  eg:反面例子,都是调用父类的eating函数

  class Human{

  public:

    void eating(void){cout<<"use hand to eat"<<endl;}

  }

  class Englishman:public Human{

  public:

    void eating(void){cout<<"use knife to eat"<<endl;}

  }

  class Chinese:public Human{

  public:

    void eating(void){cout<<"use chopsticks to eat"<<endl;}

  }

  void test_eating(Human & h)

  {

    h.eating();

  }

  int main(int argc,char **argv)

  {

    Human h;

    Englishman e;

    Chinese c;

    test_eating(h);

    test_eating(e);

    test_eating(c);

  }

  执行结果:

  use hand to eat

  use hand to eat

  use hand to eat

  

  修改test_eating函数让其能够自己判断传入的参数是属于那个类,并调用该类的eating函数(通过虚函数)

  class Human{

  public:

    virtual void eating(void){cout<<"use hand to eat"<<endl;}

  }

  class Englishman:public Human{

  public:

    virtual  void eating(void){cout<<"use knife to eat"<<endl;}//这里的virtual可以不用写,因为父类是virtual,其子类覆盖后也是virtual函数

  }

  class Chinese:public Human{

  public:

    virtual  void eating(void){cout<<"use chopsticks to eat"<<endl;}//这里的virtual可以不用写,因为父类是virtual,其子类覆盖后也是virtual函数

  }

  //void test_eating(Human * h)

  void test_eating(Human & h)//使用指针或者引用来使用对象时,才有多态,如果是test_eating(Human h)则即使使用虚函数也是调用父类的eating,因为这个时候如果调用的是test_eating(e),这个e被强制转换成Human类,会把对象里面那个指向自己虚函数表的那个指针丢掉

  {

    h.eating();

  }

  int main(int argc,char **argv)

  {

    Human h;

    Englishman e;

    Chinese c;

    test_eating(h);

    test_eating(e);

    test_eating(c);

  }

  执行结果:

  use hand to eat

  use knife to eat

  use chopsticks to eat

 说明:对于非虚函数,在编译时已经确定好调用的是那个类的函数;对于虚函数,在运行是才能确定

    对于有虚函数的类,其对象里面有个指针,指向虚函数表(虚函数表里面也是一些地址,指向虚函数);调用虚函数的时候,是通过这个指针来调用虚函数

    (通过sizeof(对象),可以发现有virtual函数和无virtual函数的对象大小不一样)(虚函数表里面出来有虚函数地址外,还有对象所属类和基类的相关信息)

  静态成员函数、内联函数、构造函数都不能是虚函数,析构函数一般都声明为虚函数(举例如下)

  int main(int argc,char **argv)

  {

    Human* h =new Human;

    Englishman* e = new Englishman;

    Chinese *c = new Chinese;

    

    Human *p[3] = {h,e,c};

    int i;

    for(i = 0;i<3;i++)

    {

      p[i]->eating;

      delete p[i];

    }

  }

  如果析构函数不是虚函数:执行结果:(在上面的各个Human、Englishman、Chinese的类中添加析构函数)

  use hand to eat

  ~Human()  

  use knife to eat

  ~Human()

  use chopsticks to eat

  ~Human()

  如果析构函数是虚函数:执行结果

  use hand to eat

  ~Human()  

  use knife to eat

  ~Englishman()

  use chopsticks to eat

  ~Chineseman()

  

  重载:函数参数不同,名字相同,不可设为虚函数(当重载函数的返回值是本类的指针或者引用时可以设置为虚函数,能实现多态);

  覆盖:函数名字、函数参数和返回值都相同,可设为虚函数;

12、类型转换

  隐式类型转换:double d = 100.1;int i =d;//double转int,i = 100;

         char *str = "100.ask";int *p = str;//char * 转为int *

  (隐式类型转换有编译器来实现,其并不一定能猜测出代码的意图,编译的时候会出现warning)

  显示类型转换:A、强制类型转换:double d = 100.1;int i =(int)d;

                   char *str = "100.ask";int *p =(int *) str;

         B、动态转换:dynamic_cast<type-id>(expression):该运算符把expression转换成type-id类型的对象。Type-id必须是类的指针、类的引用或者void*,如果tpe-id是类指针类型,那么expression也必须是一个指针;如果type-id是一个引用,那么expression也必须是一个引用;

  void test_eating(Human & h)

  {

    Englishman *pe;

    Chinese *pc;

    h.eating();

    if(pe = dynamic_case<Englishman *>(&h))

      cout<<"This human is Englishman"<<endl;

    if(pe = dynamic_case<Chinese*>(&h))//这里的&h是取址

      cout<<"This human is Chinese"<<endl;

  }

  int main(int argc,char **argv)

  {

    Human h;

    Englishman e;

    Chinese c;

    test_eating(h);

    test_eating(e);

    test_eating(c);

  }

  执行结果:

  use hand to eat

  use knife to eat

  This human is Englishman

  use chopsticks to eat

  This human is Chinese

         C、静态转换:static_cast<type-id>(expression)编译器在编译的时候已经决定好怎么转换

  下行转换会存在隐患eg:Englishman *pe = static_case<Englishman *>(&h);编译能成功,下行转换存在风险  但如果使用Englshman类的方法是程序会崩溃

             Englishman *pe = static_case<Englishman *>(&g);编译不能成功 

             Chinese*pe = static_case<Chinese*>(&g);编译能成功,没问题,上行转换没问题

            D、使用reinterpret_cast从新解析转换(同c语言风格的强制类型转换):double d = 100.1;int i =reinterpret_cast<int>(d);

         E、通过const_case去掉变量const或者volatile属性:const char *str = "100ask"; char *str2 = const_case<char *>(str);

 

注意:动态转换用于多态场合,即:必须有虚函数,引用转换的时候需要用到虚函数表里面的类和基类信息来转换

    主要用于类层次间的上行转换(派生类转换成基类)和下行转换(基类转换成派生类),还可以用于类之间的交叉转换

   在类层次间进行上行转换是,dynamic_cast和static_cast的效果是一样的;在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全

  eg:

  class Guangximan:public Chinese{

  public:

    virtual  void eating(void){cout<<"use chopsticks to eat,I come from guangxi"<<endl;}

  }

  void test_eating(Human & h)

  {

    //Englishman& pe = dynamic_case<Englishman&>(h);执行的时候会出错,转换失败,引用没指向一个实体,肯定出错

    Chinese & pc = dynamic_case<Chinese&>(h);

    Guangximan &pg = dynamic_case<Guangximan &>(h);

    h.eating();

  }

  int main(int argc,char **argv)

  {

    Guangximan g;

    test_eating(g);//Guangximan类转换成Human类就是上行转换,进入函数后Human类转换成Chinese和Guangximan 就是下行转换

  }

  

posted on 2018-05-30 09:52  拉风摊主  阅读(180)  评论(0编辑  收藏  举报

导航