C++进阶

来源:互联网 发布:武汉市广电网络分公司 编辑:程序博客网 时间:2024/05/17 20:24

C++进阶

  之前总结过一篇博客《C++程序设计》,讲了C++的基础知识点,在这篇博客中,将主要讨论C++的核心知识点。

类与对象

  我们知道类是对象的抽象,对象是类实例化的结果。 两者的关系就像设计稿和房屋一样。 首先讨论创建类的方式:

如何创建类:

复制代码
    class 类名    {public:        公用成员        ...            protected:        受保护成员        ...            private:        私有成员        ...            };
复制代码

  即首先使用关键字 class 表示在声明一个类,紧随其后添加类名,然后{},在括号中,可以定义公有成员、受保护成员和私有成员。 

  访问属性为私有的成员只能被本类中的成员函数(废话,本类除了数据成员就是成员函数了,数据成员怎么能有访问的能力,所以只能是成员函数来访问了)访问,而不能被类外访问;

  访问属性为公有的成员可以被本类中的成员函数访问,也可以在类的作用域内被其他的函数访问;

  访问属性为受保护的成员可以被本类以及本类的派生类的成员函数访问,但是不能被类外访问

  注意:

  • public 、protected、private关键字可以出现0次或者多次,但是我们应该保持这样的一种习惯: 每一种成员访问限定符只出现一次;
  • public、protected、private出现的顺序是没有要求的,但是我们推荐public在前,因为这样做的好处是突出了能被外界访问的公用成员。 但这只是习惯问题,也不必强求。
  • 在定义了类之后,最后一定要有分号来结尾。

 

对象的定义:

 1. 对象的定义方式一般是(最为常用的一种方式):

 Student zhang,wang;

  其中Student 是类名,然后实例化了zhang和wang这两个对象。。

 2. 由于c++为了兼容c,所以使用 class Student zhang,wang;也是一样的。

 3. 下面的形式也是可以的,即在定义类的时候同时声明:

复制代码
    class Name    {public:        void show()         {            cout<<a<<endl;            cout<<b<<endl;        }        private:        int a;        int b;    }zhang,wang;
复制代码

  这非常类似于c语言中的结构体。

 4. 下面的形式也可以,即不需要类名,但是一旦这样做,就说明你只实例化了这几个对象。

复制代码
    class     {public:        void show()         {            cout<<a<<endl;            cout<<b<<endl;        }        private:        int a;        int b;    }zhang,wang;
复制代码

 

 

 

类的成员函数:

  类的成员函数和普通的函数是基本相同的,区别仅在于:成员函数是某个类的,是类的一个成员,它定义在类的内部。

  且成员函数可以定义为公有的、私有的、受保护的。 只有公有的成员函数才可以被类外任意的调用。 

  成员函数可以任意的访问类内的数据成员,无论是公有的、私有的还是受保护的。 

  类中的公有成员函数是非常重要的。 一般类中的数据成员都是私有的,这就体现了封装性,而成员函数有的不需要被外界访问,那么设置成私有的。 但是如果一个类中没有公有的成员函数,这就没有意义了。

  

 

在类外定义成员函数

  成员函数可以在类内定义,但我们推荐在类外定义,因为如果在类内定义,其可读性就会大大减弱,但如果成员函数中的语句很短,只有两三行,也可以在类内定义。 在类外定义需要使用作用域运算符进行限定,如下所示。

复制代码
    class Name    {public:        void show();    private:        int a;        int b;    };    void Name::show()     {        cout<<a<<endl;        cout<<b<<endl;    }
复制代码

 

 

 

inline 成员函数

  inline函数的意思是内置函数,其思想是在编译时将被调用函数的代码直接嵌入到调用函数处。 所以这样的好处是提高了程序的执行效率。 因为如果不是这样,而是使用调用的方式,其时间开销是很大的。 

  当类中的成员函数是在类中定义时,C++系统会默认该成员函数是inline成员 函数,如果在函数定义前加上inline也是可以的,不加也行。

  但是如果成员函数定义在类的外部,类中只有成员函数的声明,那么在成员函数声明或成员函数定义前一定要有inline关键字。

 

 

 

成员函数的存储方式:

  C++中为类的对象分配内存空间时,只为对象的数据成员分配内存空间,而将对象的成员函数放在另一个公共区域,所以,无论这个类声明了多少对象,这些对象的成员函数在内存中只有一个,这样做的好处是大大节省了内存。 因为成员函数中可以有this指针啊,没有必要每次实例化一个对象的时候再创建一个函数。

  

 

对象成员的访问:

  对象成员的访问还是很简单的,主要有三种方式:

 1. 通过对象名和成员运算符访问对象的成员,即 对象名.成员名,之前我们使用的一直都是这个方式,不再赘述。

 2. 通过指向对象的指针访问对象中的成员。

  这个方法我们要使用 ->即指向运算符,该运算符可以通过指向对象的指针来访问对象的成员,举例如下:

复制代码
#include <iostream>using namespace std;class Test{public:    void Set(char ch) {c=ch;}    void Show() {cout<<"char in Test is:"<<c<<endl;}private:    char c;};int main(){    Test test1;    test1.Set('a');    Test *pTest = &test1;    test1.Show();    pTest->Show();    return 0;}
复制代码

 

最终得到的都是:char in Test is:a;

上面的代码等价于:

复制代码
#include <iostream>using namespace std;class Test{public:    void Set(char ch) {c=ch;}    void Show() {cout<<"char in Test is:"<<this->c<<endl;}private:    char c;};int main(){    Test test1;    test1.Set('a');    Test *pTest = &test1;    test1.Show();    pTest->Show();    return 0;}
复制代码

注意: 这两者的区别仅仅在于 c和this->c的不同。

举例子就是为了说明指针的这个概念, pTest指针指向的就是对象,我们可以利用指针->就可以来访问这个指针所指向的对象的成员了。 而this指针就是指向的由类创建的对象。

当然,我们这里的指针的概念并没有变,只是->会用在对象中而已, 我们可以使用(*pTest).Show()来访问成员函数。

 

3. 通过对象的引用变量来访问对象中的成员。

  这个很好理解,举例如下:

复制代码
#include <iostream>using namespace std;class Test{public:    void Set(char ch) {c=ch;}    void Show() {cout<<"char in Test is:"<<this->c<<endl;}private:    char c;};int main(){    Test test1;    test1.Set('a');    Test &test2 = test1;    test1.Show();    test2.Show();    return 0;}
复制代码

  同样,这里两次都会得到 char in Test is a;

 

 

 

构造函数和析构函数:

  构造函数和js中的构造函数有类似的作用的,但有一定的差别,在js中,构造函数是用来构造对象的,且是全部。 而在C++中,构造函数是类中的一个函数, 若要构造一个对象,构造函数是必不可少的。

  其作用是: 在创建对象时对对象的数据成员进行初始化。 如下:

复制代码
#include <iostream>using namespace std;class A{public:    A()    {        x=0;        cout<<"Constructor is Executed!"<<endl;    }    void show()    {        cout<<"x= "<<x<<endl;    }private:    int x;    };int main(){    A a1;    a1.show();    A a2;    a2.show();    return 0;}
复制代码

  最终的结果如下:

  可以看到,每创建一次对象,构造函数就会执行一次,并且将数据成员进行了初始化。 

  从上面的例子,我们可以得出以下结论(或需要注意的地方):

  • 定义构造函数的名称前不得有类型名称
  • 构造函数是在public下的,即它是公有构造函数
  • 构造函数没有返回值
  • 构造函数的作用仅仅是初始化,不得出现与无关初始化的语句。
  • 构造函数是被自动调用的,如果没有自定义的构造函数,那么系统就会创建默认的构造函数,但是默认的构造函数只是为了满足形式而已,没有任何意义。

  下面的语句是错的(因为不能在类内对数据成员进行初始化):

复制代码
class A{  ...private:    int x = 0;    };
复制代码

 

  

带参数的构造函数:

  之前的自定义构造函数的确可以进行初始化,但问题是它不能具有个性,即无论是什么对象,都做了同样的初始化操作,而带参数的构造函数可以做到有个性,如下所示:

复制代码
#include <iostream>using namespace std;class Box{public:    Box(float L,float w,float h)    {        length = L;        width = w;        height = h;    }    float Volume()    {        return length*width*height;    }private:    float length,width,height;};int main(){    Box box1(4,2,3);    Box box2(5,1,2);    cout<<"Volume of box1 is "<<box1.Volume()<<endl;    cout<<"Volume of box2 is "<<box2.Volume()<<endl;    return 0;}
复制代码

 

  最终输出如下所示:

Volume of box1 is 24
Volume of box2 is 10

 

可以看到Box()就是构造函数,并且其接受了3个形参,当然形参是由类型加变量名构成的。在构造函数中我们使用了值传递的方式给类的数据成员进行了初始化。 然后在main函数里,就可以在创建对象的时候传递实参来初始化。

 

 

 

 

构造函数与参数初始化表:

  不难看出,上面的方式来初始化还是比较麻烦的,于是C++提出了参数初始化表的方法,修改如下:

复制代码
class Box{public:    Box(float L,float w,float h):length(L),width(w),height(h){}    float Volume()    {        return length*width*height;    }private:    float length,width,height;};
复制代码

 

  这种形式的构造函数更为方便。 

 

 

构造函数的重载:

  不难理解什么是构造函数的重载,即定义多个同名但是参数不同的构造函数,根据传递参数的不同调用不同的构造函数,如下:

复制代码
#include <iostream>using namespace std;class Box{public:    Box():length(1),width(1),height(1){}    Box(float L,float w,float h):length(L),width(w),height(h){}    float Volume()    {        return length*width*height;    }private:    float length,width,height;};int main(){    Box box1(4,2,3);    Box box2;    cout<<"Volume of box1 is "<<box1.Volume()<<endl;    cout<<"Volume of box2 is "<<box2.Volume()<<endl;    return 0;}
复制代码

 

  最终结果如下:

Volume of box1 is 24Volume of box2 is 1

 

  即如果使用传递三个参数的方式,就会调用第二个构造函数,不过不传递参数(那么在名称后一定不能有()),就使用第一个构造函数。

  但是如果传递的参数对于两个构造函数都不满足,就会报错。

 

 

析构函数:

   析构函数的作用是在系统释放对象占用内存之前进行一些清理工作。析构函数的组成很简单,即~类名,即没有返回值,没有类型名,由于不需要接收参数,所以析构函数不存在重载的问题。值得注意的是,无论一个类中有多少个构造函数,析构函数只能有一个,并且析构函数如果没有自己创建,系统就会自动创建一个,但那只是形式上的析构函数,并没有任何实际的意义,举例如下:

  

复制代码
#include <iostream>using namespace std;class Box{public:    Box():length(1),width(1),height(1){}    Box(float L,float w,float h):length(L),width(w),height(h){}    float Volume()    {        return length*width*height;    }    ~Box()    {        cout<<"Destructor of Box is called!"<<endl;    }private:    float length,width,height;};int main(){    Box box1(4,3,5);    Box box2;    cout<<"Volume of box1 is "<<box1.Volume()<<endl;    cout<<"Volume of box2 is "<<box2.Volume()<<endl;    return 0;}
复制代码

 

 

   执行结果如下所示:

 

Volume of box1 is 60
Volume of box2 is 1
Destructor of Box is called!
Destructor of Box is called!

 

 由于创建来了两个对象,所以析构函数执行了两次。  可以看出析构函数就是破坏者函数的意思。

 

 

构造函数和析构函数的调用次序:

  先构造的后析构,后构造的先析构。构造函数的调用顺序和创建对象的顺序是一致的。而析构函数的调用顺序和构造函数是相反的。

 

 

对象数组:

  对象数组和普通的数组并没有什么本质的区别,只不过普通的数组的元素是简单变量,而对象数组的元素是对象而已。下面举一个简单的例子了解即可。

复制代码
#include <iostream>using namespace std;class Box{public:    Box()    {        length = 1;width = 1; height = 1;        cout<<"Box("<<length<<","<<width<<","<<height;        cout<<") is constructed!"<<endl;    }    Box(float L, float w, float h)    {            length = L; width = w; height = h;        cout<<"Box("<<length<<","<<width<<","<<height;        cout<<") is constructed!"<< endl;    }    float Volume()    {        return length*width*height;    }    ~Box()    {        cout<<"Destructor of Box"<<length<<","<<width<<","<<height;        cout<<") is called!" <<endl;    }private:    float length,width,height;        };int main(){    Box Boxs[3] = {       Box(1,3,5),       Box(2,4,5),       Box(3,6,9)    };    return 0;}
复制代码

 

 

  最后输出如下:

Box(1,3,5) is constructed!Box(2,4,5) is constructed!Box(3,6,9) is constructed!Destructor of Box3,6,9) is called!Destructor of Box2,4,5) is called!Destructor of Box1,3,5) is called!

 

  

 

 

对象指针:

 指向对象的指针和普通的指针并没有什么太大的区别,只是现在的指针指向的是内存中对象所占用的空间的首地址,即对象在内存中的首地址称为对象的指针,用来保存对象指针的指针变量称为指向对象的指针变量,简称为指向对象的指针。

 说明: 指针实际上一般是指针变量的简称,真正指针的意思是内存的首地址,而保存这个首地址的变量为指针变量。

 

 对象指针比较特殊的地方在于,定义了一个对象指针之后,需要使用->来访问对象的公有数据成员和公有的成员函数,如下:

A *pa = NULL;A a;pa = &a;pa -> data = data;pa -> fun();

 

即先定义了一个对象指针,然后定义了一个对象,这个指针指向这个对象,然后访问了对象的数据成员和对象的成员函数。

 

指向对象成员的指针

  刚才说的是指向对象的指针,而这里要说的是是指向对象成员的指针,分两种情况来讨论:

第一指向对象数据成员的指针

  这和普通的指针完全相同,如下所示:

数据类型名 *指针变量名

指针变量 = &对象名.数据成员名;

  

复制代码
#include <iostream>using namespace std;class A{public:    A(int a,int b,int c)    {        x = a; y = b; z = c;    }    void show()    {        cout<< x << y << z << endl;    }    int x,y,z;    };int main(){        A first(5,5,5);        first.show();        int *p = NULL;        p = &first.x;        cout<<*p<<endl;}
复制代码

  最终输出的结果是555        5。  

  即这里使用了 p = &first.x; 这种方式就可以使得指针p指向对象的数据成员。

 

第二:指向对象成员函数的指针(非常重要)

  指向对象成员函数的指针和指向普通函数的指针是由区别的,区别在于:

  • 定义指向对象成员函数的指针时需要在指针名前加上所属的类名及域运算符“::”
  • 指向对象成员函数的指针不仅仅要匹配将要指向函数的参数类型、个数和返回值类型,还要匹配将要指向函数所属的类。
  1. 指向普通函数的指针变量定义如下:  返回值类型(*指针名)(参数表); 
  2. 指向成员函数的指针变量定义如下: 返回值类型(类名::*指针名)(参数表)
  3. 使用指向成员函数的指针指向一个公用成员函数的语句如下:指针名 = &类名::成员函数名
  4. 使用指向成员函数的指针调用对象的成员函数的语句如下: (对象名.*指针名)(实参表)

  下面给出一个例子:

  

复制代码
#include <iostream>using namespace std;class Box{public:    Box()    {        length = 1;width = 1; height = 1;        cout<<"Box("<<length<<","<<width<<","<<height;        cout<<") is constructed!"<<endl;    }    Box(float L, float w, float h)    {            length = L; width = w; height = h;        cout<<"Box("<<length<<","<<width<<","<<height;        cout<<") is constructed!"<< endl;    }    float Volume()    {        return length*width*height;    }    ~Box()    {        cout<<"Destructor of Box("<<length<<","<<width<<","<<height;        cout<<") is called!" <<endl;    }private:    float length,width,height;        };int main(){    Box box(2,2,2); // 创建Box的对象box    float(Box::*p)();  // 定义指向Box类的成员函数的指针p,因为成员函数没有参数,所以这里也没有参数    p=&Box::Volume;   // 给指针p赋值,使其指向Box类的成员函数Volume;    cout<<"Volume of box is "<< (box.*p)() << endl; // 调用指针p指向的函数    return 0;}
复制代码

 

  最终的输出如下所示:

Box(2,2,2) is constructed!Volume of box is 8Destructor of Box(2,2,2) is called!

 

  注意下面的几点:

  • 由于物理上成员函数是独立于对象存在的,所以我们用p=&Box::Voluem而不是p=&box::Volume;
  • 调用指向对象成员函数的指针指向的成员函数时,要通过(对象名.*指针名)(实参表)的形式,其中的对象名不能替换为类名。
  • 定义指针时就可以初始化。

  

 

this指针

  this指针是指向本类对象的指针,它的指向是被调用成员函数所在的对象,即调用哪个对象的该成员函数,this指针就指向哪个对象。

  在成员函数内部引用数据成员的前面隐藏着this指针,如之前的length*width*height实际上是(this->length)*(this->width)*(this->height),进一步,如果是调用box1的Volume函数,那么就是(box1->length)*(box1->width)*(box1->height)。

  我们常用的就是在构造函数中更加清楚地表达。如下:

复制代码
    Box(float length, float width, float height)    {            this.length = length;        this.width = width;        this.height = height;        cout<<"Box("<<length<<","<<width<<","<<height;        cout<<") is constructed!"<< endl;    }
复制代码

  这样的表达显然比之前的表达更为清楚一些。

 

 

 

对象与const

  常对象: 即 const 类名 对象名[(实参表)],注意其中【】表示可选。 这样就定义了一个常对象,也可以写成  类名 const 对象名[(实参表)],两者是等价的。

  另外, 如果一个对象被声明成了常对象,那么不能调用该对象的非const形的成员函数(除了系统自动调用的隐式的构造函数和析构函数)。

    常对象成员:包括常数据成员和常成员函数

  注意:常数据成员的声明和作用与普通的常变量相似,也是使用const来声明,且在程序运行过程中数据成员的值不能修改。 常变量在声明时也必须同时初始化。但是应当记住数据成员在初始化时必须使用构造函数的参数初始化表。 

  例如在Box类中,我们将length属性已经定义为了常数据成员,然后要初始化,下面的初始化方法是非法的:

Box::Box(float L, float w, float h)

{

  length = L;

  width = w;

  heigth = h;

}

   这时就会报错,因为length已经被声明为了常变量,所以其初始化要通过列表形式,如下是正确的:

Box::Box(float L, float w, float h):length(L)

{

  width = w;

  height = h;

}

   

  上面说了常数据成员的形式,下面说一下常成员函数的形式:即将成员函数声明为const型,这样的成员函数不能不改类对象的数据成员的值,否则就会报错。 

  声明常成员函数的一般形式为:

        返回值类型 成员函数名(形参表) const;

   即规则就是直接在普通的函数声明后面添加const关键字,而在调用时不必添加const。

 

  不同类型的成员函数与数据成员之间的引用关系

数据成员|成员函数常成员函数非常成员函数常数据成员可以引用,但不可以修改值可以引用,但不可以修改值非常数据成员可以引用,但不可以修改值可以引用,也可以修改值常对象的数据成员可以引用,但不可以修改值不允许引用和修改值

  

 

指向对象的常指针:

  指向对象的常指针是指将指向对象的指针声明为const类型,这样指针在定义且赋初值以后,在程序执行的过程中不能再发生改变,即这个指针不能再指向其他对象了。定义方法如下:

类名 * const 指针名 [= &类的对象];

  

 

指向常对象的指针变量

  定义指向常对象的指针变量的一般形式为:  const 类名 * 指针变量名

  (1)如果一个对象已经被声明为常对象,只能用指向常对象的指针变量指向它, 而不能用一般的指针变量去指向它。如下所示:

复制代码
#include <iostream>using namespace std;class Clock{public:    Clock(int h, int m, int s)    {        hour = h;        minute = m;        second = s;    }    void Display()    {        cout<<hour<<":"<<minute<<":"<<second<<endl;    }    int hour,minute,second;};int main(){    const Clock clock1(1,1,1);// 定义Clock类对象clock1,它是一个常对象    const Clock *p1 = &clock1; // 合法 定义指向常对象的变量指针    Clock *p2 = &clock1;  // 非法  因为p2是一个一般的指针    ....    return 0;}
复制代码

 

  (2) 如果定义了一个指向常对象的指针变量,并使得他指向一个非const的对象,那么他指向的对象是不能通过指针来改变的,如:

    Clock clock2(2,2,2);    const Clock *p2 = &clock2;    p2->hour = 2; // 非法    p2->Display(); // 非法

  (3) 如果定义了一个指向常对象的指针变量,虽然不能通过他改变所指向的对象的值,但是指针变量本身的值是可以改变的。

  (4) 指向常对象的指针变量最常用于函数的形参,目的是保护形参指针所指向的对象,使得它在函数的执行过程中不被修改,如下:

复制代码
#include <iostream>using namespace std;class Clock{public:    Clock(int h, int m, int s)    {        hour = h;        minute = m;        second = s;    }    void Display()    {        cout<<hour<<":"<<minute<<":"<<second<<endl;    }    int hour,minute,second;};int main(){    void fun(const Clock *p);    Clock clock(10,10,10);    fun(&clock);    return 0;}void fun(const Clock*p){    p->hour = 12; // 错误    cout<<p->hour<<endl; //正确}
复制代码

 

 请记住这样的一条规则:当希望在调用函数时对象的值不被修改,就应当把形参定义为指向常对象的指针变量,同时用对象的地址作为实参(对象可以使const或非const形)。

 

 

 

 

对象的常引用

  对象的常引用即对象的一个别名,定义如下:

const 类名 & 引用名 = 对象名;

   特点是,无法通过引用来修改对象的数据成员。 且必须在定义的时候初始化。

 

 

对象的动态创建和释放

  前面所定义的对象都是静态的,在程序的运行过程中,对象所占的空间是不能随时释放的。 但是有时候我们希望在需要用到对象的时候再创建对象而不需要的时候就撤销它,释放他所占有的内存以供别的数据使用。这样可以提高内存空间的利用率。

  若希望动态创建就要使用new了,new的作用就是分配内存空间。即  new Box; 可以动态地创建一个Box类的对象,如果执行成功,就会从内存堆中分配一块内存空间,来存放Box类的对象,紧接着调用构造函数初始化对象。如果内存分配成功,new运算符就会返回分配的内存的首地址;如果内存分配失败,则会返回一个NULL,但是通过new 运算符动态创建的对象没有名字,所以在使用new动态创建对象时要声明一个指针变量来保存对象的首地址,如:

  Box *pc = new Box;

且我们还可以在new创建的时候给出实参,调用带有参数的构造函数初始化对象,如: Box *pc = new Box(2,2,2);  动态创建之后,就可以通过指针向访问普通的对象一样来访问对象的公用成员了。

  如: 

  pc -> Volume();

刚才说了,如果创建失败,就会返回NULL,所以为了保险起见,最好的方式是先判断,即

Box *pc = new Box(2,2,2);

if (pc == NULL)

{pc->Volume();}

   当我们不需要使用这个对象的时候,就可以使用delete来释放这个对象了,即:

  delete p;

   这样就可以将对象所占用的内存归还给堆。 注意:new创建的对象只能delete释放内存。

  注意: 一旦使用new创建了对象并将地址给了指针,那么这个指针就不要再指向别的地方了,否则会出现问题,如不能delete对象。

   如下:

复制代码
using namespace std;class Clock{public:    Clock(int h, int m, int s)    {        hour = h;        minute = m;        second = s;    }    void Display()    {        cout<<hour<<":"<<minute<<":"<<second<<endl;    }    int hour,minute,second;};int main(){    Clock *p = new Clock(17,48,30);    p->Display();    delete p;    return 0;}
复制代码

  即当我们不需要这个对象时,直接使用delete释放内存归还给堆即可。

 

 

对象的赋值和复制

   对象的赋值

  相同数据类型的数据之间是可以相互赋值的,同理,相同类的对象之间也可以相互赋值。一个对象的值可以赋给另一个对象,这种赋值运算是通过=运算来实现的。

  格式:

 对象名1 = 对象名2;

   赋值操作完成的是将对象2中的数据成员的值赋值给对象1中的数据成员。 而成员函数是没有这种赋值的关系的。

  注意:

  • 不同对象之间的赋值操作只对其中的数据成员赋值,而不对成员函数赋值,因为不同的数据成员占用不同的内存空间,但是对象的成员函数是共享同一段代码的。 因此不需要也无法对成员函数进行赋值操作。

  

  对象的复制

  对象的赋值是在创建对象时使用已有的对象快速地复制出完全相同的对象,在C++中赋值对象的形式如下:

  类名 对象2 = 对象1;

   这种情况下,创建对象2的系统会调用一个称谓“复制构造函数”的特殊的构造函数。其作用是将对象1的各数据成员的值一一复制给对象2中的数据成员。

 

对象的赋值和复制的比较

  • 同: 对象的赋值和复制大部分情况下都是把一个对象的数据成员依次赋给另一个同类对象的相应数据成员。
  • 异: 对象的赋值是建立在两个对象都存在的基础上的,而对象的复制是用一个已有的对象复制一个新的对象时进行的。
  • 异: 两者调用的函数不同,对象的赋值调用的是赋值运算重载函数,而对象的赋值调用的是赋值构造函数。 

 

 

 

向函数传递对象

  向函数传递对象与普通变量的参数传递是一样的,也可以分为值传递、地址传递和引用传递3种,细节不再赘述。

 

 

 

 

继承与组合

 继承和派生: 一个新类从已有的类那里获得其已有特性,这种现象称为类的继承。于是就出现了基类和派生类,基类是派生类的抽象, 派生类是基类的实例化结果。 根据派生类继承的个数不同,可以分为单继承和多继承等 。派生类继承了基类的所有的数据成员和成员函数,但是不会继承构造函数和析构函数。

 派生类的声明方式: 即 

class 派生类名:【继承方式】基类名{    派生类增加的成员;};

  其中继承方式包括公有继承(public)、私有继承(private)和保护继承(protected),且它是可选的,如果没有声明继承方式,默认是私有继承。 

 

   派生类的构成:派生类从基类继承来的是共性,而派生类自己添加的是个性。 但是这绝不是简单的叠加。 一般分为三个步骤:

    第一步: 从基类继承 --- 即接受所有的成员函数和数据成员(构造函数、析构函数除外),这一步,要慎重选择基类。

    第二步:调整从基类接受的成员---调整一: 通过不同的继承方式,如私有继承,就会将基类所有的成员在派生类中表现为私有的,不能在外部访问。调整二: 可以在派生类中定义和基类相同的变量名以达到覆盖的目的。

    第三步: 添加新的成员---这一步的过程就是添加个性的过程。

 

 三种继承方式剖析: 公有继承、私有继承、保护继承

  公有继承: 这种继承方式需要添加关键字public,这样公用基类的公有成员在公有派生类中还是公有的受保护成员在公有派生类中还是受保护的,而基类中的私有成员在派生类中是不可访问的,只能通过基类提供的接口来间接访问。

  私有继承: 私有继承添加关键字private,如果什么都不添加,默认也是私有继承。在私有基类中的公有成员和保护成员都会变成派生类的私有成员, 即不能在类(即类这个结构)外访问。而基类中的私有成员在继承类中仍然不能被继承类访问,只能通过基类的接口间接的对基类的私有成员进行访问。

  保护继承:保护继承需要添加关键字 protected, 即基类中的公有成员和保护成员都会变成派生类的保护成员,而基类中的私有成员仍然是私有的,不可以被派生类访问,而只能通过积累成员间接的访问。

  受保护成员和私有成员的区别是什么? 私有成员说明只能在类内访问,而不能在类外访问(包括派生类)。 但是保护成员,虽然不能在类外访问,但是可以在派生类中访问,也就是说一个类中一旦有了受保护成员,那么这个类很有可能就是为了作为基类的。 这样派生类就可以访问受保护成员了。 

  

  派生类对基类成员的重定义和名字隐藏:即如果我们再派生类中定义了和基类同名的成员,那么当我们使用派生类访问的时候,访问的一定是派生类中新增的,如果我们希望能够继承基类的同名成员,那就需要使用基类限定符,如:

obj.Base::show();

  这样就可以访问到基类的show()函数了,如果仅仅是obj.show()那么我们访问的是派生类的show函数。

 

  

 派生类的构造函数:我们知道派生类是不能继承基类的构造函数和析构函数的,所以说我们必须学会在派生类中定义构造函数,以完成初始化。举例如下:

复制代码
#include <iostream>using namespace std;class Base{public:    Base(int m,int n){x=m;y=n}    ~Base(){}protected:    int x;    int y;};class Derived:public Base{public:    Derived(int m,int n,int k):Base(m,n)    {        z=k;    }    void show()    {        cout << "x="<<x<<endl;        cout << "y="<<y<<endl;        cout << "z="<<z<<endl;    }    ~Derived(){}private:    int z;};int main(){    Derived obj(12,34,56);    obj.show();    return 0;}
复制代码

 

 这就是派生类的构造函数的形式,即在派生类的构造函数中调用基类的构造函数,仅仅初始化新增的数据成员。

注意: 通过派生类创建一个对象,会先调用基类的构造函数,然后再调用派生类的构造函数

 

 派生类析构函数

 这个并没有什么特别的,只要知道他的执行顺序即可。

 

 

 多重继承

 多重继承和单继承是类似的,包括继承的方式书写,析构函数的书写方式都是一样的。但是值得注意的是,如果多个基类有“重名”的情况,那么我们再继承类对象obj访问时就会发生错误,如obj.show()如果两个基类都有这个函数,就会报错了,方法是通过限定符,如obj.Base1::show()或obj.Base2::show()来访问就不会出错了。(说明,这里所说道的错误就是指二义性

 

 虚基类

 如果A是基类,B继承了A,C也继承了A,然后D继承了A和B,那么这时候会出现的情况就是D中含有双份的成员(一般情况下),但是这是浪费内存的,所以我们需要解决这个问题,即使用了虚基类,直接上例子,如下所示:

复制代码
#include <iostream>using namespace std;class Base{public:    Base(int m, int n):x(m),y(n){}protected:    int x,y;};class Base1: virtual public Base{public:    Base1(int m,int n,int k):Base(m,n),{z1=k;}protected:    int z1;};class Base2: virtual public Base{public:    Base2(int m,int n,int p):Base(m,n),{z2=p;}protected:    int z2;};class Derived:public Base1,public Base2{public:    Derived(int m,int n,int o, int p, int q):Base(m,n),Base1(m,n,k),Base2(m,n,p)    {        z = q;    }    void show()    {        cout<<"x="<<x<<endl;        cout<<"y="<<y<<endl;        cout<<"z1="<<z1<<endl;        cout<<"z2="<<z2<<endl;        cout<<"z="<<z<<endl;    }protected:    int z;};int main(){    Derived obj(12,13,14,15,16);    obj.show();    return 0;}
复制代码

 

即我们通过虚基类来继承,这样最后就不会出现重复的情况。这也就是虚基类的作用。

指的注意的是:我们并不提倡在程序中使用多重继承,只有在比较简单和不易出现二义性的情况或实在必要时才使用多重继承,能用单一继承解决的问题不适用多重继承。且Java等语言根本就不支持多重继承。

 

组合:

  继承和组合不同。 如一个汽车是由轮胎、方向盘、车身。。。组合出来的,这就是组合。 也就是说继承是纵向的,而组合是横向的。 

  

 

多态性和虚函数:

   一门语言如果只支持对象而不支持多态性,那么就不是面向对象的,最多说成是基于对象的,如VB就是这样的语言。 

  C++不仅支持对象,还支持多态,这就是典型的面向对象的语言。

  多态,通俗的理解就是---向不同的对象发出同一个消息,不同的对象在接收到消息之后会有不同的相应,这就是多态。

  通过多态,我们可以实现“一个借口,多种方法”。

  在C++中,多态可以分为4类: 参数多态、包含多态、重载多态和强制多态。 前两种为通用多态,后两者为专用多态。

  • 参数多态:如函数模板和类模板就是参数多态。 由函数模板实例化的各个函数都具有相同的操作,而这些函数的参数类型却各不相同。 
  • 包含多态:主要是通过虚函数来实现的,是研究类族中定义于不同类中的同名成员函数的多态行为。
  • 重载多态:如前面介绍的函数重载。 还有运算符重载都是重载多态。
  • 强制多态:是指将一个变元的类型加以变化,以符合一个函数(或操作)的要求,如加法运算符在进行浮点数和整型数相加时,首先进行的是类型强制转换,把整型数变为浮点数再相加的情况就是强制多态的实例。

  

向上类型转换:

  向上类型转换是指把一个派生类的对象作为基类的对象来使用。向上类型转换有三点需要注意:

  • 向上类型转换是安全的
  • 向上类型转换需要自动完成
  • 向上类型转化不会丢失子类型信息

  下面是一个向上类型转换的典型例子:

复制代码
#include <iostream>using namespace std;class B0{public:    void display()    {        cout<<"B0::display()"<<endl;    }};class B1: public B0{public:    void display()    {        cout<<"B1::display()"<<endl;    }};class D1: public B1{public:    void display()    {        cout<<"D1::display()"<<endl;    }};void fun(B0 *ptr){    ptr->display();}int main(){    B0 b0;    B1 b1;    D1 d1;    B0 *p;    p = &b0;    fun(p);    p = &b1;    fun(p);    p = &d1;    fun(p);    return 0;}
复制代码

 

可以看到最终的输出结果如下:

B0::display()B0::display()B0::display()

 

即在调用时,会将B0的派生类的对象转化为B0的对象,即函数fun()接受一个B0类的对象,但也不拒绝任何B0派生类的对象。 B1类和D1类到B0类的向上类型转换会使B1和D1的接口变窄,但是不会小于B0。

 

 然而这并不是我们想要的结果。。。那就继续吧。。

 

  

 

功能早绑定和晚绑定:

  所谓绑定就是将函数体和函数调用想联系。这里联系是一个关键词。

  当绑定在程序编译阶段就完成时,称之为早绑定。 当绑定在在程序运行阶段完成的是晚绑定,即当程序调用某一个函数名时才去寻找和连接其程序代码,这就是晚绑定。

  显然,早绑定的效率更高,但是灵活性不足,而玩绑定的效率可能会低一些(因为需要临时去查找),但就是因为这样,其灵活性更好一些。

  C++虽然是编译形语言,和c一样,C是只支持功能早绑定,但是C++提出了虚函数的概念,进而可以实现晚绑定。即C++是早绑定和晚绑定结合的。

 

 

实现功能晚绑定 --- 虚函数

  虚函数就是在函数前添加virtual关键字,一般这个函数应当是在基类中的成员函数,定义了这个虚函数后,它就会告诉C++编译器在编译的时候不要进行早绑定,而是在执行的时候在取寻找对应的函数体。

  于是我们可以把代码修改如下:

复制代码
#include <iostream>using namespace std;class B0{public:    virtual void display()    {        cout<<"B0::display()"<<endl;    }};class B1: public B0{public:    void display()    {        cout<<"B1::display()"<<endl;    }};class D1: public B1{public:    void display()    {        cout<<"D1::display()"<<endl;    }};void fun(B0 *ptr){    ptr->display();}int main(){    B0 b0;    B1 b1;    D1 d1;    B0 *p;    p = &b0;    fun(p);    p = &b1;    fun(p);    p = &d1;    fun(p);    return 0;}
复制代码

  即将基类的display()函数定义为了虚函数,这样就可以实现功能晚绑定了。 最终的结果如下:

B0::display()B1::display()D1::display()

  这是因为虚函数的晚绑定,使得程序根据prt所指向的实际对象来调用函数。

  关于虚函数的几点说明

  (1) C++规定,如果在派生类中,没有使用virtual显示地给出虚函数声明,那么这时系统就会遵循以下的规则来判断一个成员函数是不是虚函数:

  • 该函数与基类的虚函数有相同的函数名
  • 该函数与基类的虚函数有相同的参数和返回值
  • 该函数与基类的虚函数有相同的返回类型且满足赋值兼容规则的指针。

  一旦满足了上述几个条件,就会认为派生类中的这个函数是虚函数。

   书150页

  (2)必须首先在基类中定义虚函数。

  (3)派生类中对虚函数重写时,virtural可以写也可以不写。

  (4)虽然使用对象名和点运算符的方式也可以调用虚函数,如语句“c.message()”可以调用虚函数“car::message()”,但是并没有利用到虚函数的特性。故只有通过基类指针或引用访问虚函数时才能获得运行时的多态性(多理解几遍!结合上面的例子)。

  (5)一个虚函数无论被调用多少次,仍然保持虚函数的特性。

  (6)内联函数不能是虚函数、构造函数不能是虚函数、析构函数可以是虚函数

 

  

析构虚函数

  在析构函数前面加上关键字“virtual”进行说明,则称该虚构函数为虚析构函数。语法是:

  virtual ~类名();

  注意:如果将基类的析构函数声明为虚函数时,则由该基类所派生的所有派生类的析构函数也都自动成为了虚函数,即使派生类的析构函数与基类的析构函数名字不相同。

重要:当基类的析构函数为虚函数时,无论指针指向的是同一类族中的哪一类对象,系统都会采取动态关联,调用相应的析构函数,对该对象进行清理工作。 最好把基类的析构函数定义为虚函数,这样,所有派生类的析构函数自动就成了虚函数,这样,如果程序中显示地使用了delete运算符准备删除一个对象,所有的对象会被释放。

 

注意:虚函数只能是成员函数,而重载函数是什么都可以。 重载函数处理的是同名的函数, 虚函数不完全是。

 

   纯虚函 数是指一个在基类中说明的虚函数,他在该基类中没有定义,但要求在他的派生类中必须定义自己的版本,或重新说明为纯虚函数。如:

class Shape{   virtual void show() = 0;   virtual void area() = 0;  }

 其中的show和area就是纯虚函数,而Shape是基类,纯虚函数只是声明了,但是并没有具体的定义,定义的工作需要后续的派生类来完成。

 

 

抽象类:

  如果一个类至少有一个纯虚函数,那么就称该类为抽象类

  • 由于抽象类至少包含一个没有定义功能的纯虚函数,所以,抽象类只能作为其他类的基类来使用,不能建立抽象类对象
  • 不允许从具体类中派生出抽象类
  • ...

 

 

 

面向对象的妥协 --- 友元函数

  友元可以访问与他有好友关系的类中的任何成员。  有元包括友元函数和有缘类。

  优点:有元增强了便利性      缺点:有元使得封装性遭到了严重破坏。

友元函数可以是一个普通的函数也可以是另一个类中的成员函数,下面分别举例说明:

复制代码
#include <iostream>using namespace std;class Show{public:    Show(int a);    friend void display(Show &);private:    int age;};Show::Show(int a){    age = a;}void display(Show &b){    cout<<b.age<<endl;}int main(){    Show s(21);    display(s);    return 0;    }
复制代码

 

好久不写C++,自己写时出了问题:

  • class Show是在声明一个对象,在对象体后要有分号作为结尾。
  • 因为构造函数放在了外面,所以要在函数体中声明,并且在外部定义时要写 类名::,注意:构造函数无返回值,名称和类名相同。
  • 在主函数的结尾,一定要写return 0;因为主函数一定要有返回值 

 对于友元函数,需要知道:

  • 这个例子中友元函数是类外的一个普通函数,在类中声明这个函数是有元函数,需要在函数声明前添加关键字 friend 。 
  • 声明这个函数是要说明 display 是我的朋友,只要他来访问,就让他来吧,  即display函数是不属于这个类的,只是这个类的朋友,所以可以访问, 即b.age。

 

友元函数还可以是一个类中的成员函数:

  情况类似,这里不再举例。需要注意的是,如果在一个类中使用到了另一个还有没有定义的类,就会报错。我们需要提前声明这个类。

 

友元类

  不仅可以将一个函数声明为一个类的朋友,还可以将一个类声明为另一个类的朋友,这就是友元类。若B类是A的友元类,那么有元类B中的所有成员函数就是A的友元函数,也就是说B中的有元函数可以访问A类中的所有成员。

  如老师类是学生类的有元类,可以作如下声明:

复制代码
#include <iostream>#include <string>using namespace std;class Student;class Teacher{public:    void assigngrades(Student &s);protected:    int stu_count;    Student *plist[100];    };class Student{public:    friend Teacher;    ...    protected:    int num;    string name;    float score;};...
复制代码

  这样,老师类就是学生类的友元类了。可以看到,我们只需在学生类中声明 friend Teacher; 即提醒自己老师是我们的朋友,她来访问我的时候我得接受。。。。

 

关于友元需要注意的几个地方:

  • 友元函数的声明可以出现在类中的任何地方(包括private和protected部分), 也就是说友元的说明不受成员访问控制符的限制。
  • 友元关系是单向的而不是双向的。
  • 友元关系是不能传递的 。

在实际工作中,并不会把某个类声明为另一个类的友元类,除非真的必要。

 

 

 

 

对象机制的破坏 --- 静态成员:

  静态成员包括静态数据成员和静态成员函数。

  我们知道一个类创建对象的时候,会为每个对象的数据成员分配内存,然后不同的数据成员的值是不同的。这样的缺点是: 同一个类中的对象无法实现资源共享,当然,如果希望资源共享,我们可以使用全局变量,但是全局变量容易被污染,这是我们不推荐的。那么资源共享的好处是什么呢? 比如我们创建一个对象,让共享的数据成员+1那就好了。 但是目前的数据成员做不到,于是引出了静态数据成员的概念。

  静态数据成员在类中需要在声明前添加static关键词,然后这个数据成员就可以被所有的类共享了。

  值得注意的是,静态数据成员是在创建类的时候就分配了内存的,而在创建实例对象的时候,所有的对象共享这个内存,所以他们不会再回静态数据成员分配内存。

  且不仅仅每个对象可以访问静态数据成员(前提它是公有的public,如果是私有的,只有静态成员函数可以访问,后面会讲到),类也可以访问这个静态数据成员,因为他是属于类的。

  且通过一个对象修改静态数据成员,那么通过其他对象(或类)再次访问时其值就也会相应的做出改变。

  

  静态数据成员不能在类中进行初始化,而只能在类外进行初始化,初始化的时候不需要再使用static关键词,如果我们不初始化,那么系统就会自动初始化默认为0。

  如下所示:

复制代码
class Teacher{public:    static int a;protected:    int stu_count;    };int Student::a = 46;
复制代码

 

   这就是一个最简单的设定静态数据成员和初始化的代码。  

 

静态成员函数:

  静态成员函数的设定其实是为了静态数据成员服务的,当我们把静态数据成员设置为私有的之后,我们就不能通过类或对象来访问了,而只能通过静态成员函数来访问。  

  定义静态成员函数只需要在函数的最前面添加static关键词。 然后我们就可以通过调用静态成员函数来间接的访问私有的静态数据成员了。

  • 使用类来访问静态数据成员 Student::getCount()
  • 使用对象来访问静态数据成员 s1.getCount()

 说明:

  • 静态成员函数不能默认访问本类中的非静态成员
  • 静态成员函数可以直接访问本类中的静态数据成员

  在C++中最好养成这样的习惯 --- 只用静态成员函数访问静态数据成员,而不访问非静态数据成员。 这样思路清晰,逻辑清楚,不易出错。

 

 

2017年4月2日更新:

运算符重载:

什么是运算符重载,为什么要进行运算符重载?

  int a = 5, b = 10;  float c = 15, d = 20;  那么int e = a + b; 以及 float f = c + d;中的两个加号就是+这个运算符的重载, 设置我么还可以写成  float g = e + f; 这些都是运算符的重载。

  通过运算符的重载,使得我们的运算表现的更为简洁。

  但是如果使用Student类创建了两个对象,Student s1,s2;  那么s1+s2可以实现相加吗?  显然这是不行的,我们并没有对之进行预定义。

 

运算符重载的目的

  运算符重载的目的是将系统中已经定义的运算符用于新定义的数据类型,从而使同一个运算符作用于不同类型的数据导致不同的行为。

 

说明: 这一部分的内容还是很多的,鉴于此部分支持暂时用的不多,我会总结一些比较重要的知识点,更为详尽的知识以后继续看:

  

运算符重载实例

  注意: 运算符的重载实质上是函数的重载。 

复制代码
#include <iostream>#include <string>using namespace std;class Complex{public:    Complex(double r = 0, double i = 0) {real = r; imag = i;}    Complex operator + (Complex &);    void display(string);private:    double real,imag;};Complex Complex::operator + (Complex &c2){    return Complex(real + c2.real, image + c2.image);}void Complex::display(string str){    cout<<str<<"=("<<real<<","<<imag<<"i)"<<endl;}int main(){    Complex a(3,8),b(5,7),c,d;    d = a + b;    c = a.operator + (b);    c.display("c = a + b");    d.display("d = a + b");    return 0;}
复制代码

  由此可以看出重载运算符的规则。

 

重载运算符需要注意的几个地方:

  • 运算符的重载只能在C++现有的基础上进行重载,并且含义应当是相当的。
  • 运算符的重载不能改变运算符的优先级和顺序。
  • 运算符的重载不能改变原有的操作数。

 

 

 

 

模板

  这是我觉得C++中最后的一块比较重要的地方了,嗯,看了两三天的C++了,终于可以告一段落了,加油!

为什么需要模板?

  比如如果我们希望实现一个简单的比较大小的函数,其中类型包括 int long 和 double,那么我们就需要作出下面的工作(因为C++是典型的强类型语言,需要对每一种数据规定严格的数据类型,而js则完全不同):

复制代码
#include <iostream>using namespace std;int max(int x, int y) {return x>y?x:y;} //整数比较double max(double x, double y) {return x>y?x:y;}    //长整数比较long max(long x, long y){return x>y?x:y;} // 实数比较int main(){    int a = 5,b = 8,m;    long c = 48745, d = 25413,n;    double e = 2.235, f = 5.236,p;    m = max(a,b);    n = max(c,d);    p = max(e,f);    cout<<"int_max"<<m<<endl;    cout<<"long_max"<<n<<endl;    cout<<"double_max"<<p<<endl;    return 0;}
复制代码

 

   最终结果输出如下:

int_max8long_max48745double_max5.236

 

   虽然可以实现功能,但是这样做的代价太大,做了很多重复性的工作。我们也可以通过宏定义来解决此问题,但是宏定义由于只是简单的文字替换,所以会带来更多的问题,可以认为这是一种反函数,所以我们认为最好还是不要使用宏定义。

  有鉴于此,我们需要一种新的方式来解决这个问题,这就是模板。 

 

模板实例:

  所谓函数模板,实际上就是建立一个通用的函数,其函数类型和形参类型的全部或部分类型不具体指定,同一个虚拟的类型来代表。这个通用的函数就称为函数模板。 凡是函数体相同的函数都可以使用这个模板来代替,不必定义多个函数,只需要在模板中定义一次即可。 在函数调用时系统会根据实参的类型来取代模板中的虚拟类型,从而实现了不同函数的功能。

复制代码
#include <iostream>using namespace std;template <typename T>T max(T x, T y){    return x>y?x:y;}int main(){    int a = 5,b = 8,m;    long c = 48745, d = 25413,n;    double e = 2.235, f = 5.236,p;    m = max(a,b);    n = max(c,d);    p = max(e,f);    cout<<"int_max"<<m<<endl;    cout<<"long_max"<<n<<endl;    cout<<"double_max"<<p<<endl;    return 0;}
复制代码

 

  上面这就是一个模板函数的使用,但是在我的基于MINGW的编译器上跑不起来,提示出错,不知何故。

  基本的语法就是先template 声明这个一个模板,然后<typename> T是说这个类型的变量名称是T,即typename的首字母,当然也可以不用T,但是人们已经习惯了使用T,另外typename也可以写成class,但是不要和类class相混淆。 最后,就是定义了函数体,然后根据不同的调用来确定不同的T然后调用。

  说明:

  • 在定义时 template <typename T>和函数体之间不应当出现其他语句。
  • 不要把class和typename搞混,推荐使用typename,这样意思更加明确。
  • 函数模板的类型参数可以不止一个,如 template <class T1, class T2, class T3> T1 fun (T1 a, T2 b, T3 c){}

  

   对于之前的模板,如果调用如下: max(2.3, 2); 即前者是double类型,后者是int类型,那么就会报错,错误时不匹配类型。  因为之前的例子都是要么使用int实例化,要么使用double实例化。 

  解决方法有三:
  其一:强制类型转化

  max( int(2.3),2 ); 或者max( 2.3, double(2) ); 

   这两种方法都可以使得传入的参数强制类型转化为统一的形式。

  

  其二: 通过提供"<>"里的参数来调用这个模板,如下所示:

  max<double>(2.3, 2); 或者 max<int>(2.3, 2);

   前者就可以告诉编译器用double实例化函数模板,那么传进来的参数就会由相应的类型转换。而后者可以告诉编译器用int进行调用,然后进行相应的类型转化。

 

  其三: 制定多个模板参数,如下:

T1 max(T1 x, T2 y){    return x>y?x:y;}

 

   这样的函数模板就支持两个参数了,而不需要我们进行类型的强制转换或者<>形式的强制调用。

 

 

模板形参表:

  在函数模板中,不仅可以用typename或class关键字声明的类型参数,还可以出现确定类型的参数,如:

  tymplate <class T1, class T2, class T3, int T4>

  T1 fun (T1 a, T2 b, T3 c) {....}

   上述模板中T4是非类型参数,它代表了一个潜在的值。 它被用作一个常量,可以出现在函数的其他部分。

  注意:非类型参数是受限制的,它可以是整型、枚举型、对象或函数的引用,以及对象、函数或类成员的指针,但是不允许使用浮点型(或双精度形)、类对象或void作为非类型参数。

0 0
原创粉丝点击