关于C++的问题

来源:互联网 发布:dota2赌博软件 编辑:程序博客网 时间:2024/04/20 20:02

关于C++的问题

标签(空格分隔): C/C++


固定链接:https://www.zybuluo.com/SiberiaBear/note/208237

以下文字中部分代码是我随便写的,难免出错,望见谅。

  • 关于C的问题
    • 野指针
    • delete函数只能释放堆上开辟的内存
    • 函数默认参数
    • 名字粉碎
    • 注意重载函数调用二义性
    • 循环语句定义作用域的争议
    • C中的struct
    • 常量成员函数
    • 系统自动添加的默认构造函数
    • 默认构造函数二义性接10
    • 复制构造函数
    • 成员初始化列表
    • 类静态数据成员
    • 重定义与重载
    • 派生类构造函数只负责直接基类的初始化
    • 构造函数和析构函数的调用时间与次序
    • 多重继承方式下的二义性
    • 没有虚拟继承下成员函数的二义性
    • 虚基类由最终派生类初始化接17
    • 派生类对象向基类对象赋值
    • 友元运算符重载
    • 重载二元运算符中第1个参数的类型转换接23
    • 类模板中的模板参数
    • 实例化模板类成员数时模板类成员函数的实例化
    • 类模板的对象或其引用作为函数参数
    • 构造函数与析构函数不能显式调用
    • 友元的类间引用
    • 联编
    • 类对象作为函数参数与以类作为返回值类型的函数
    • 引用概念
    • 用构造函数实现类型转换
    • 复制构造函数与赋值运算符函数
    • 重载运算符和
    • 内联函数
    • 常对象与常成员函数
    • const常成员函数virtual虚函数static静态成员函数在类体外定义的样式
    • 虚析构函数
    • 重载new和delete运算符

1.野指针

野指针是指在delete了一个指向动态对象的指针后,没有及时置为NULL,如果对该指针进行解除引用,就会产生垃圾值。一个铁的纪律,彻底杜绝野指针,delete了一个指向动态对象的指针之后,及时置为NULL,相应的,对指针进行解除引用前,判断指针是否为NULL。
参考:http://www.cnblogs.com/yc_sunniwell/archive/2010/06/28/1766854.html

int main(){    int *p = new int;    *p = 2;    cout << "p=" << p << endl;    cout << "&p=" << &p << endl;    cout << "*p=" << *p << endl;    delete p;    //p = NULL;         //注释A    cout << "p=" << p << endl;    cout << "&p=" << &p << endl;    //cout << "*p=" << *p << endl;      //注释B    return 0;}

注释A句是比较重要的,如果不加这句话,delete p后,p指向的地址为00008123,如果无意再使用该指针(是可以使用的),注释B句解引用时,程序会崩溃,这时的指针就是野指针。不过,我尝试了一下,任何一个指针在delete之后,系统都会把00008123地址给这个指针,也算是编译器的一种补偿吧。为了安全起见,还是最好在free或delete时,同时将指针指向NULL。

另外还有一种野指针的成因是初始化一个指针之后,忘记让其指向一个对象,但同时也没有让其指向NULL,所以会导致这个指针随意指向。

2. delete函数只能释放堆上开辟的内存

如下:

int a = 6;delete &a;      //运行时报错

第二句在运行时报错,原因是a是一个局部变量,存储在栈上,delete this试图释放栈上的内存,所以会报错。

3. 函数默认参数

在指定某个函数的默认参数时,如果它有函数原型,就只能在函数原型中指定对应参数的默认值,不能在函数定义时再重复指定参数的默认值。如果一个函数的定义先于其调用,没有函数原型,若要指定参数的默认值,需要在定义时指定。

double sqt(double f=0);double sqt(double f) {       return f*f;}/*错误,函数已声明,不能在定义时指定参数默认值double sqt(double f=0){       return f*f;}*/

一个函数若具有多个默认参数值时,所有默认参数都必须出现在右边,一旦某个参数开始指定默认值了,它右边的所有参数都必须指定默认值。

int f(int i, int j=0, int m=0);     //正确int g(int i, int j=0, int m);       //错误int h(int i=0, int j, int m=0);     //错误

4. 名字粉碎

c++在编译程序时,会利用形式参数表重新命名每个重载函数的名字,称为名字粉碎,或函数签名。其方法是系统为每种数据类型指定一个简单的代码,如用i代表int,用d代表double,名字粉碎的方法就是依照形式参数表的次序,将每个参数的类型代码附到重载函数名之后,并由此形成重载函数的名称,故而,每个同名的重载函数,在程序编译之后,都会有不同的名字。

5. 注意重载函数调用二义性

int f(int & x) {...}double f(int x) {...}

这两个重载函数,如果有一句定义:

int a=1;f(a);

应该如何调用?
事实上,系统编译会出现二义性,因为调用两个函数都是合法的,前者调用的是变量a的引用,后者调用的是变量a的值。要避免这种二义性。

6. 循环语句定义作用域的争议

对于for和while循环语句,标准C++规定在其循环测试条件中定义的名字,其作用域也限于循环本身,即结束于循环体结束的右括号}

void f1(int z) {    for(int i=0; i<z; i++)    {        int j=i;        cout << i*j << endl;    }    cout << i << endl;          //错误,因为此时i已经不存在了,在作用域之外}

但是,许多C++编译器中,这段程序能够正确编译和运行,原因是在标准C++之前,上边的for循环是按照如下方式处理的:

int i=0;for(; i<z; i++){...}

现在,许多编译器仍然按照这种方式处理for循环,如VC++ 6.0以及其前边的版本。在visual c++ 6.0中,for循环中定义变量的作用域为包括此for循环的程序块,因此在VC6.0中,i在for循环之外仍然有意义,但它与标准C++的规范不符合。

7.

当用指针或引用从函数中返回一个地址时,一定不要返回局部变量的指针或引用。

#include <iostream>using namespace std;int * f1() {    int temp =1;    return &temp;}int * f2() {    int t = 99;    return &t;}void main() {    int *p;    p = f1();    cout << *p << endl;    f2();    cout << *p << endl;}

程序的输出结果是:

199

从程序上看,两次都输出1才对,因为第二次并没有把f2()的函数返回值给指针p,所以p应该没有变,其实,这个问题是函数返回局部变量引起的,第二次输出99只是巧合,第二次输出可以是任何数,因为f1()函数中,temp只是局部变量,它的生存期仅仅在函数f1()存在时存在,所以当f1()调用结束后,将temp的地址返回给p,然后释放掉temp,第一次输出时,原temp地址中的数据并没有被任何数据覆盖,因为两句话之间没有其他程序,但是第二次输出时,因为执行了f2(),产生了其他数据,将原地址中的数据覆盖,所以指针p指向的地址所在的单元的内容就会发生未知的改变。在本例中,刚巧f2()中创建的局部变量t所开辟的内存空间正是p所指向的内存空间,也就是两个函数中的局部变量开辟的是同一片内存空间,所以会出现99,如果函数代码量大,就不容易产生这种巧合。

8. C++中的struct

C++中的struct本身也是一种类,它与class具有着相同的功能,用法完全相同,但是他们的唯一区别是,当没有指定成员的访问权限时,struct中默认具有public权限,而在class中默认具有private权限。

9. 常量成员函数

只有类的成员才能定义常量函数,普通函数不能定义常量函数。常量函数与常量参数有区别,常量参数限制函数对参数的修改,但与数据成员被修改无关,而常量成员函数限制的是对类中数据成员的修改。

10. 系统自动添加的默认构造函数

只有在类没有定义任何构造函数时,系统才会产生默认构造函数,一旦定义了任何形式的构造函数,系统就不会产生默认构造函数,如果此时需要调用默认构造函数时,系统就会出错,提示没有合适的构造函数可用(找不到无参的构造函数)。如11。

11. 默认构造函数二义性(接10)

如果显式的定义了无参数的默认构造函数,又定义了全部参数都有默认值的构造函数,就容易在定义时产生二义性。

class X{public:    X() { x=0;}    X(int i=0) { x=i;}private:    int x;};void main() {    X one(12);          //调用第二个构造函数;    X two;              //不知道调用哪个构造函数;    X *p = new X;       //不知道调用哪个构造函数;}

注意全部参数都有默认值的构造函数不等同于默认构造函数,前者是指在函数参数表中,所有的参数都被指定了默认值,而后者是指函数参数表中没有任何参数的构造函数,该构造函数叫做默认构造函数,由于如果程序员未在类中显式的加入任何构造函数,当由该类来声明一个对象时,编译器会自动为它添加一个默认构造函数,从而来声明对象。如果该类中程序员加入了任何构造函数(不一定是默认构造函数),则编译器不会调用默认构造函数,此时,如果某个对象需要通过默认构造函数才能够声明时,就会造成编译错误。
如果上例中,类中为:

class X{public:    X(){}    X(int x, int y=0) {a = x; b = y;}       //Aprivate:    int a;    int b;};

这样时,主函数中不会出错,因为A句并不是具有全部参数默认值的构造函数,而是具有部分参数默认值的构造函数。另外,X(int x=0, int y){a = x, b = y;}这样的构造函数并不存在,会报错,因为成员初始化列表中有要求,见第3条。

另:如果一个类中同时具有无参构造函数和全部参数都有默认值的构造函数,也可以定义对象数组,因为对象数组并不需要给每个对象创建参数初始值,所以不会引起二义性。
另:当将默认构造函数的内容指定了全部参数的默认值,可以认为默认构造函数和具有全部参数默认值的构造函数的作用是一样的,如下:

X(){ a = 0, b = 0; }X(int x = 0, int y = 0) { a = x; b = y; }

事实上,编译器在处理这两种构造函数时,是按同样的样式对待的。

12. 复制构造函数

对于:

X obj1;X obj2 = obj1;          //调用复制构造函数X obj3(obj1);           //调用复制构造函数f(X o);                 //以对象作为函数参数,调用复制构造函数

千万不要把第一句理解成:

X obj2;obj2 = obj1;

这样的话,程序将先调用无参构造函数建立obj2,然后再用赋值语句将obj1的值赋值给obj2,不会调用复制构造函数,如果显式的重载了赋值运算符函数,则会调用这个自定义的赋值运算符函数来完成赋值工作,否则将会调用系统默认赋值运算符来完成赋值工作。注意,如果类中存在指针数据成员,这样赋值会引起指针悬挂问题。

13. 成员初始化列表

下列类成员必须采用成员初始化列表的方式进行初始化:常量成员、引用成员、类对象成员、派生类构造函数对基类构造函数的调用。派生类构造函数对基类构造函数的调用就是指派生类继承基类时,其构造函数需要负责对其直接基类的构造。

#include <iostream>using namespace std;class A {    const int i, j;    int &k;public:    A(int a, int b, int c) :i(a), j(b), k(c) {        //i = a; j = b; k = c;          //A        cout << "i=" << i << "\t" << "j=" << j << "\t" << "k=" << k << endl;    }};int main(){    int m = 6;    A x(4, 5, m);    return 0;}

上例中,初始化成员i, j为常量成员,成员k为引用成员,都需要在初始化列表中初始化,如果用注释A的语句初始化而不用成员初始化列表,则会报错:必须初始化常量限定类型的对象 必须初始化引用

14. 类静态数据成员

静态数据成员也需要在类内声明,遵守类的限定规则,但静态数据成员还需要被定义,因为类的声明中将一个数据成员指定成静态成员,它只是一种声明,由于静态数据成员在所有该类的对象所共有,所以只有一份内存空间,不会随着类对象的建立而建立,所以需要给静态数据成员在类外做定义,只有被定义,该类静态数据成员才会被分配内存空间。(一些编译器,如果类的静态数据成员没有在类外被定义,则会报错,而另一些编译器,如VC++ 6.0,则不会报错,而是在定义该类的第一个对象时定义相关的静态数据成员)

在类外定义静态数据成员和静态成员函数时,不能加上static限定词,只能在类内声明时加。类外定义静态数据成员时,可以指定初始值,如果未指定,系统会默认给其初值为0;如下是静态数据成员的类外定义:

class A{    static int number;      //0};int A::number;      //1int A::number=1;    //2

如第0句是静态数据成员的声明,第1句是静态数据成员的定义(未指定初始值),第2句是静态数据成员的定义(指定初始值为1)。

静态成员函数没有this指针,所以静态成员函数不能访问对象的非静态数据成员,因为非静态数据成员是通过this指针传递给成员函数的,这也就是静态成员函数只能访问静态数据成员的原因。静态成员函数可以在定义任何类的对象之前即可访问类的静态数据成员,但是其必须在定义了该类对象之后访问类的非静态数据成员。

静态数据成员和静态成员函数即可以通过对象名调用,也可以通过类名来调用:

void main() {    cout << A::number << endl;    cout << A::staticfunc() << endl;}

全局变量也可以实现静态数据成员的功能,但是全局函数破坏了程序的封装性,可以允许任何代码随意修改全局变量,给程序维护和安全性带来了负担。静态成员函数为操作静态数据成员提供了一种接口,可以保护静态数据成员的安全性。

可以用static声明类对象成员为静态成员,但是不可以用auto、register、extern限定类域内变量的定义。

15.

对象成员的构造次序与他们在类中的声明次序相同,而与他们在构造函数初始化列表中的次序无关。

C(int i1, int i2, int i3, int i4): b1(i1), a1(i2), b2(i3), a2(i4){}

不管初始化列表顺序怎么变,输出只与类中声明次序一致。

16. 重定义与重载

派生类可以添加基类没有的新成员,也可以对基类的成员函数进行重定义或重载。重定义是指派生类可以定义与基类具有相同函数原型的成员函数(即具有相同的返回类型、函数名及参数表),而重载则要求成员函数具有不同的函数原型。
需要指出,派生类对基类成员函数的重定义或重载会影响基类成员函数在派生类中的可见性,基类的同名成员函数会被派生类重载的同名函数所隐藏。

class Base{    int xpublic:    void setx(int i){ x=i;}    void set(int n){ x=n;}    void setx(int m){ x=m;}    void print(){...}};class Derived:public Base{    void set(int p, int k){ m=p; n=k;}    void set(int i, int j, int k){ ...}    void print(){...}};void main(){    Derived d;    d.set(1, 3);        //正确,调用Derived类中的set函数    d.set(5,6,7);       //正确,调用Derived类中的set函数    d.set(10);          //错误,无法调用基类Base中的set函数    d.Base::set(10);    //正确,可以通过指明基类类名来调用被隐藏的基类重载成员函数    d.setx(5);          //正确,setx()函数并没有被派生类重载或重定义,所以可以调用    d.print();          //这样调用的是Derived类中的print()函数,基类中的print()函数被重定义了,所以隐藏了    d.Base::print();    //这样才会调用Base类中的print()函数}

注意:也有人认为派生类与基类同名成员函数之间是一种重定义而非重载的关系,其理由如下:
- 重载并不会隐藏同名的其他成员函数名。
- 重载是指相同作用域中的同名函数,而派生类与基类各自定义了一个唯一的作用域,这两个作用域各自独立。

17. 派生类构造函数只负责直接基类的初始化

C++标准中有一条规则:如果派生类的基类同时也是另外一个类的派生类,则每个派生类只负责它的直接基类的构造函数调用。
这条规则表明,当派生类的直接基类只有带参数的构造函数,没有默认构造函数时,它必须在构造函数的初始化列表中调用其直接基类的构造函数,并向基类的构造函数传递参数,以实现派生类对象中的基类子对象的初始化。
这条规则有一个例外,在虚拟继承中,如果存在间接虚基类,派生类必须负责它的初始化工作。(链接21)

18. 构造函数和析构函数的调用时间与次序

当派生类具有多个基类和对象成员时,他们的构造函数都将在创建派生类对象时被调用,调用次序如下:

基类构造函数 -> 对象成员构造函数 -> 派生类构造函数
  • 当有多个基类时,按照他们在继承方式中的声明次序调用,与他们在构造函数中的初始化列表次序无关。
  • 当有多个成员对象时,将按照他们在派生类中的声明次序调用,与他们在构造函数中的初始化列表次序无关。
  • 当构造函数初始化列表中的基类和对象成员的构造函数调用完成后,才执行派生类构造函数体中的程序代码。
  • 析构函数与构造函数调用次序刚好相反。

19. 多重继承方式下的二义性

如果一个派生类通过多继承方式同时继承两个基类,而这两个基类中含有同名的成员函数,则在派生类对象中调用该成员函数会造成二义性,有四种解决办法:

  1. 在这种情况下,如果需要调用指定哪个基类里的同名成员函数,需要显式的指出调用哪个基类。如:
    调用基类A中的同名成员函数f():mi.A::f();
    调用基类B中的同名成员函数f():mi.B::f();
  2. 在类中定义同名成员。与基类成员同名的派生类成员将屏蔽对基类成员的直接访问,叫做支配原则。
  3. 虚基类。虚基类保证了一个类不能从同一个类中直接继承一次以上。
  4. 各基类中的成员各不相同,当然,这不是很好的解决办法。

另:二义性检查在访问控制权限或类型检查之前进行,所以访问控制权限不同或类型不同不能解决二义性问题,即在两个基类中,成员名相同,即使一个为private,一个为public也是不能避免二义性;两个基类中,成员名相同,但只有返回值类型不同,也是不能避免二义性。

20. 没有虚拟继承下,成员函数的二义性

如果一个派生类通过多继承方式同时继承两个基类,同时这两个基类又继承于同一个间接基类,则在派生类中调用间接基类中的函数时,因为从基类1中也可以找到一个间接基类里的该函数,也可以从基类2中找到另一个间接基类里的同样内容的函数,所以这个函数会引起二义性,虽然无论用哪个成员函数都是一样的,但是会造成编译错误。
这个问题必须通过虚拟继承解决。

21. 虚基类由最终派生类初始化(接17)

在没有虚拟继承时,每个派生类的构造函数只负责其直接基类的初始化,但是在有虚拟继承方式下,虚基类由最终派生类的构造函数负责初始化。最终派生类是指在对层次的继承结构下,创建对象时所用的类。

22. 派生类对象向基类对象赋值

可以把派生类对象赋值给基类对象、把派生类对象的地址赋值给基类指针、把派生类对象作为基类对象的引用。但是,不能翻过来成立。因为派生类中包含基类的所有内容,所以可以将派生类中的基类内容赋给基类对象,但是基类中缺失派生类中新增加的内容,无法赋值给派生类。

23. 友元运算符重载

调用类的重载运算符时,作为类成员函数运算符的左参数必须是一个类对象,而作为友元或普通函数重载的运算符则无此限制。如:

Complex a, b(2, 3);a = b + 2;      //正确a = 2 + b;      //错误

这个问题只能通过友元函数或普通函数的运算符重载实现。

friend Complex operator+ (Complex a, int b);friend Complex operator+ (int a, Complex b);

友元运算符函数重载的好处是: 二元运算符左边一个值可以是非类对象的数据类型,而类成员运算符函数则要求二元运算符左边一个值必须是该类对象,同时,友元函数可以在参数不匹配的情况下,对二元运算符左边一个值进行类型转换,而类成员运算符函数则不会对这个值进行类型转换。(链接24)

class A {    int r;public:    A(){ r = a; }    A(int a){ r = a; }    A operator+(int a);    friend A operator-(int a, A b);    friend A operator-(A a, A b);};A A::operator+(int a) {    return A(r + a);}A operator-(int a, A b) {    return A(a - b.r);}A operator-(A a, A b) {    return A(a.r - b.r);}int main() {    A a, b;    b = a + 2;      //调用类成员运算符函数,正确    b = 2 + a;      //2无法进行类型转换,错误    b = 2 - a;      //调用友元运算符函数1,正确    b = a - 2;      //调用友元运算符函数2,对2进行类型转换,成为:b = a - A(2); 正确    return 0;}

24.重载二元运算符中第1个参数的类型转换(接23)

对于重载的二元运算符而言,如果运算符的第2个参数与要求的类型不匹配,C++将进行所有可能的隐式类型转换。但是对于第1个参数,就要分情况了:对于非类成员的重载运算符函数,C++编译器在参数不匹配的情况下将对第1个参数进行隐式类型转换,但不会对作为类成员运算符函数的第1个参数进行任何隐式类型转换。
所以,对于不要求左值并可以交换参数次序的二元运算符,最好用非成员函数形式的重载运算符函数实现,包括友元和普通函数。

25.

operator<<的第一个参数必须是ostream类对象的引用,所以输出运算符的重载不能作为类的成员函数,因为类的成员函数重载运算符必须要求运算符的第一个参数必须是类的对象。同理,输入运算符的重载也不能是类的成员函数。

26.

如果基类定义了虚函数,当且仅当通过基类指针或引用调用派生类对象时,才能实现多态性,访问到它们实际所指向的虚函数版本。当通过普通基类对象访问派生类对象时,不能实现虚函数的特性,只能访问到派生类中的基类子对象中的成员。

27.

只有类的非静态成员函数才能定义为虚函数,类的构造函数和静态成员函数不能定义为虚函数,原因是虚函数需要在继承结构中发生作用,而构造函数和静态成员函数是不能被继承的。
内联函数不能被定义为虚函数,因为内联函数是静态联编的方式,而虚函数是动态联编的方式。
但是析构函数可以被定义为虚函数,这样做虽然违背了虚函数的形式,但是可以最大程度上析构掉所有继承结构下的类对象内容。如:

#include <iostream>using namespace std;class A{public:    ~A(){cout << "call A::~A()" << endl; }};class B: public A{    char * buf;public:    B(int i){ buf = new char[i];}    ~B(){        delete []buf;        cout << "call B::~B()" << endl;    }};void main() {    A *a = new B(10);    delete a;}    

输出结果为:

call A::~A()

在这个例子中,对派生类B中的数组并没有调用析构函数,只调用了基类的析构函数来析构基类对象a,但基类对象a是开辟了B类类型的内存,也就是创建了B类中的一个buf,但并未释放,出现内存泄漏。
故而,这时就需要虚函数的析构函数:

class A{public:    virtual ~A(){...}};class B:public A{public:    virtual ~B(){...}       //这里不写virtual,B的析构函数也仍然是虚函数};

28.

抽象类是含有纯虚函数的类,由于纯虚函数没有实现代码,故而抽象类不能建立类的对象,只能作为其他类的基类(但是,类中如果有虚函数但没有纯虚函数时,是可以创建对象的,虚函数需要被定义。如果某个派生类继承了该抽象类,但是仍然没有给纯虚函数以实现代码,则该派生类也是抽象类。
纯虚函数是指在声明时被初始化为0的虚类成员函数。纯虚函数在基类中声明,但它在基类中没有具体的函数实现代码,要求继承它的派生类为纯虚函数提供实现代码。

class X{    virtual return_type func_name(param) = 0;};

29. 类模板中的模板参数

类模板中的模板参数有两种类型:类型参数和非类型参数,类型参数是指模板参数表中用classtypename限制的参数,它代表任意数据类型,在模板调用时需要用实际类型来代替。非类型参数是指某种具体的数据类型,在调用模板时只能为其提供相应类型的常数值。非类型参数是受限制的,通常可以是整型、枚举型、对象或函数的引用,以及对象、函数或类成员的指针,但不允许用浮点型(或双精度型)、类对象或void作为非类型参数

30. 实例化模板类成员数时,模板类成员函数的实例化

在实例化模板类成员时,并不会生成实例化类模板的成员函数,也就是说,在用类模板定义对象时并不会生成类成员函数的代码。对于从未被调用的成员函数,永远也不会生成它的函数代码。类模板成员函数的实例化发生在该成员函数被调用时,这就意味着只有那些被调用的成员函数才会被实例化。或者说,只有当成员函数被调用了,编译器才会为它生成真正的函数代码。

31. 类模板的对象或其引用作为函数参数

与普通类的对象一样,类模板的对象或其引用也可以作为函数的参数,只不过这类函数通常是模板函数,且其调用实参常常是该类模板的模板类对象。

32. 构造函数与析构函数不能显式调用

构造函数与析构函数不能在程序中显式调用它,将由系统自动调用。

33. 友元的类间引用

友元使编程更简洁,程序运行效率也更高。但它可以直接访问类的私有成员,破坏了类的封装性和信息隐藏。
类也可以定义为另一个类的友元,称为友元类,友元类中的每一个成员函数都是另一个类的友元函数,也可以定义类中某个成员函数是另一个类的友元成员函数。定义为:

class B;            //注释1:是必须的,因为类A中的成员函数将B作为参数,而此时B还未建立,则需要前向声明class A{    //注释2:    friend class B;     //定义了类B是类A的友元类    int func(B b);};class B{};class C{        friend int A::func(B b);    //定义了类A的一个成员函数是类C的友元成员函数};int A::func(B b) { return 3;}   //注释3:函数的定义只能在C类定义之后
  • 注释1:第一句声明是必须的,当前边的类A中使用了后边的类B时,而类B还未定义,需要提前声明。
  • 注释2:一个类的成员函数作为另一个类的友元时,必须先定义这个类。类C中引用了类A的成员函数做友元,如果先定义类C是不行的。
  • 注释3:友元成员函数必须在两个类都定义了之后才能再定义,友元成员函数可以将两个类结合起来,使这两个类之间的工作高效。

34. 联编

联编也称为绑定。联编分为静态联编和动态联编。静态联编是指在程序执行前,编译器根据函数调用提供的信息,在程序编译时就把调用函数名与具体的函数所绑定在一起。动态联编是指在程序编译时还不能确定函数调用所对应的具体函数,只有在程序运行过程中根据具体的数据类型才能够确定函数调用所对应的具体函数,即在程序运行时才把调用函数名与具体函数绑定在一起。
静态多态性是通过函数重载和运算符重载在程序编译时通过静态绑定实现的,动态多态性是通过继承和虚函数在程序执行时通过动态绑定实现的。平时所说的多态性,多指动态多态性。
静态多态性执行速度快,动态多态性执行速度慢,但提供了更多的灵活性、问题抽象性和程序的可维护性。

35. 类对象作为函数参数与以类作为返回值类型的函数

有两个知识点:

  • 有多个类对象作为函数参数时,这些形式对象参数的构造函数调用次序是从右向左依次调用,析构函数调用次序与构造函数调用次序相反,是从左到右依次调用。
  • 函数返回值类型为类类型时,有两种情况。情况一:return后边重新定义了一个类类型的对象,这种情况函数不会再创建一个临时的类对象来存储返回值;情况二:return后边直接接一个已经被定义的类的对象,这种情况函数会另外创建一个临时的类对象来存储该返回值的对象(调用复制构造函数)。无论是情况一还是情况二,这个重新定义的对象(或函数创建的临时对象)会在函数全部结束之后最后销毁(调用析构函数)。

举个栗子:

#include <iostream>using namespace std;class Complex {    double real, imag;public:    Complex() { real = 0; imag = 0; cout << "默认构造函数" << endl; }    Complex(double r, double i) {         real = r;         imag = i;         cout << "构造函数" << "this:" << this->real << endl;     }    Complex(Complex &c) {         real = c.real;        imag = c.imag;        cout << "复制构造函数" << "this:" << this->real <<endl;     }    ~Complex() { cout << "析构函数" << "this:" << this->real << endl; }    Complex &operator=(const Complex &c);    void display() { cout << real << "+" << imag << "i" << endl; }    double getreal() { return real; }    double getimag() { return imag; }};Complex Add(Complex c, Complex d) {    return Complex(c.getreal() + d.getreal(), c.getimag() + d.getimag());}Complex Equal(Complex c) {    Complex d(5,5);    return d;}Complex MM(Complex c1, Complex c2, Complex c3) {    return Complex(c1.getreal() + c2.getreal() + c3.getreal(), c1.getimag() + c2.getimag() + c3.getimag());}Complex &Complex::operator=(const Complex &c) {    real = c.real;    imag = c.imag;    cout << "重载赋值运算符" << "this:" << this->real << endl;    return *this;}int main(){    Complex c1(1, 1), c2(2, 2), c3(3, 3);    //c3 = Add(c1, c2); c3.display();           //注释A    //c3 = Equal(c1);       c3.display();       //注释B    //c3 = MM(c1, c3, c2);  c3.display();       //注释C    return 0;}

在本例中,定义了一个类Complex,两个私有数据成员,同时显式定义了默认构造函数、构造函数、复制构造函数、析构函数、重载赋值运算符函数,另外定义了分别具有1个、2个、3个Complex类对象作为函数参数的函数,其中函数Equal()的return值是一个已经存在的对象,同知识点二的第二种情况;而函数Add MM的return值是一个新定义的类的对象,同知识点二的第一种情况;函数MM()指定多个参数,来验证知识点一的陈述。
为了方便,我们将各个构造函数和析构函数等都加this指针引用第一个私有成员real,从而能方便的发现当前的函数是哪个对象的。

当打开注释A时,显示的内容是:

构造函数this:1构造函数this:2构造函数this:3复制构造函数this:2复制构造函数this:1构造函数this:3析构函数this:1析构函数this:2重载赋值运算符this:3析构函数this:33+3i析构函数this:3析构函数this:2析构函数this:1

当打开注释B时,显示的内容是:

构造函数this:1构造函数this:2构造函数this:3复制构造函数this:1构造函数this:5复制构造函数this:5析构函数this:5析构函数this:1重载赋值运算符this:5析构函数this:55+5i析构函数this:5析构函数this:2析构函数this:1

当打开注释C时,显示的内容是:

构造函数this:1构造函数this:2构造函数this:3复制构造函数this:2复制构造函数this:3复制构造函数this:1构造函数this:6析构函数this:1析构函数this:3析构函数this:2重载赋值运算符this:6析构函数this:66+6i析构函数this:6析构函数this:2析构函数this:1

36. 引用概念

  1. 在定义引用时,引用符&与指针运算符一样,在类型和引用名之间的位置是灵活的。
  2. 在变量声明时出现的&符号才是引用运算符(包括函数参数声明和函数返回值类型声明),其他地方出现的&都是地址操作符。
  3. 引用代表一个变量的别名,必须在定义时初始化,不能在定义完成后再给它初始化。可以给一个变量指定多个引用。
  4. 一个引用名只能作为一个变量的别名,无法将它再次指定为其他变量的别名。
  5. 引用实际上是一种隐式指针,但它与指针存在不同,如下。
  6. 当用&运算符获取一个引用的地址时,实际取出的是引用对应的变量的地址。注:关于引用变量到底占不占用内存空间,是一个争论已久的话题,因为在C++标准中并没有详细说明引用的实现,各种C++编译器的实现也不同。
  7. const引用时存在的,可以用const限制对变量的修改。如:
int i = 2;int &m = i;const int &a = i;m = 3;      //正确i = 3;      //正确a = 3;      //错误

const引用可以用常量初始化,const int &k = 3;是正确的,如何实现与编译器相关。
8.建立引用时,需要注意:不能建立引用的引用;不能建立引用数组,也不能建立数组的引用;可以建立指针的引用,但不能建立指向引用的指针。

int i = 0;int a[10];int &aa = a;        //错误,无法建立数组的引用int &ia[5];         //错误,不能建立引用数组int &*ip = i;       //错误,不能建立指向引用的指针int &&ii = i;       //错误,不能建立引用的引用int *pi = &i;int *&pr = pi;      //正确,可以建立指针的引用

9.引用可以作为左值存在。

37. 用构造函数实现类型转换

如下例题:

#include <iostream>using namespace std;class Data {private:    int year;    int month;    int day;public:    Data(int YEAR=2000, int MONTH=10, int DAY=10) { year = YEAR; month = MONTH; day = DAY; cout << "具有全部参数的默认构造函数" << endl; }    Data(Data &d) { year = d.year; month = d.month; day = d.day; cout << "复制构造函数" << endl; }    ~Data() { cout << "析构函数" << endl; }    Data &operator=(const Data &d) {         year = d.year;         month = d.month;         day = d.day;         cout << "重载赋值运算符函数" << endl;         return *this;     }    void show() {        cout << year << "/" << month << "/" << day << endl;    }};int main(){    Data d(2000, 5, 5);    d.show();    d = 1995;       //注释A    d.show();    return 0;}

该例中,注释A行,系统遇到将一个int型的常量赋值给类对象,将调用Data类的构造函数(具有全部默认值的构造函数)来将该int型常量转换成Data型的临时对象,然后再将该临时对象通过赋值运算符(重载赋值运算符函数)来传递给类对象d,这其中没有调用到复制构造函数。输出如下:

具有全部参数的默认构造函数2000/5/5具有全部参数的默认构造函数重载赋值运算符函数析构函数1995/10/10析构函数

38. 复制构造函数与赋值运算符函数

在C++中,存在两种可以将一个类向另一个类完全复制的函数,是复制构造函数与赋值运算符函数,两者的不同之处在于,复制构造函数是负责类新对象的初始化赋值,而赋值运算符函数是负责已经建立的类的赋值操作。
复制构造函数:

class A{}void main(){    A a(10);        //调用复制构造函数    A a=10;         //调用复制构造函数    A b;            //调用默认构造函数    b = a;          //调用赋值运算符函数}

两者都可以在程序员不显式指定的情况下由编译器自动生成最小函数,如果没有显式指定,使用编译器自动生成的函数,两者都会出现在含有指针作为类成员的类赋值中出现指针悬挂的问题。

39. 重载运算符++和–

我们需要重载自加运算符++和自减运算符–,有个问题要先明确。这两个运算符作为前缀时,是可以修改整个表达式的值的,因为可以作为左值;如果作为后缀时,是不可以修改整个表达式的值的,因为不是左值,如:

int n, m;n =1;m = ++(++n);    //正确,(++n)本身可以被++m = (n++)++;    //错误,(n++)本身不可以被++

好,明确这个之后,我们看一下重载自加运算符和自减运算符时的前后缀表示:

class X{    X operator++() {...}        //++作为前缀    X operator++(int) {...}     //++作为后缀    friend X operator--() {...} //--作为前缀    friend X operator--(int) {...}  //-作为后缀}

int参数本身不会被调用,只是作为一个标记,来告诉编译器,这个重载运算符时后缀,而不是前缀,所以在给函数定义时也不需要指定int的变量值。
接下来看一个成功重载这两个运算符函数的一个栗子:

#include <iostream>using namespace std;class Counter {private:    int n;public:    Counter(int i = 0) { n = i; }    Counter &operator++();    const Counter operator++(int);    friend Counter &operator--(Counter &c);    friend const Counter operator--(Counter &c, int);    void display();};Counter &Counter::operator++() {            //前缀++,按正常的方式,前缀是可以被修改的,即(++a)是一个左值    ++n;    return *this;}const Counter Counter::operator++(int) {        //后缀++,按正常的方式,后缀是不可以被修改的,即(a++)不是一个左值,不能出现在等号左边    Counter temp = *this;    ++n;    return temp;}Counter &operator--(Counter &c) {       //前缀--    --c.n;    return c;}const Counter operator--(Counter &c, int) { //后缀--    Counter temp = c;    --c.n;    return temp;}void Counter::display() {    cout << "counter number=" << n << endl;}int main(){    Counter a,b;    b = ++a;    a.display();    //a=1    b.display();    //b=1    b = a++;    a.display();    //a=2    b.display();    //b=1    b = --a;    a.display();    //a=1    b.display();    //b=1    b = a--;    a.display();    //a=0    b.display();    //b=1    return 0;}

只有这样写才能实现前边所述的功能。

40. 内联函数

如果一个成员函数的说明和定义都在类体内,则该成员函数被默认定义为内联函数。定义在类体外的成员函数必须在成员函数定义前加上关键字inline,来显式的说明为内联函数。然而,网上对于这个论题的争论不一,有些人认为,类内的成员函数是否被定义为内联函数取决于编译器,有些编译器在定义内联函数时是灵活的,如果类内定义的成员函数被程序调用时经过了多重循环等操作时,编译器也不会将其强制定义为内联函数。

内联函数是将函数内容在编译时就直接替换掉程序中调用该函数的部分,而不采用函数调用的方式在程序运行时调用,从而提高了程序运行效率,是一种以空间换取时间的编译方式。

注意:

  • 在内联函数内不允许使用循环语句和开关语句。
  • 内联函数的定义必须出现在内联函数第一次调用之前。

41. 常对象与常成员函数

常成员函数不会更新对象的数据成员,也不能调用该类中没有用const修饰的成员函数。常成员函数可以修改被定义为mutable的成员变量。

常对象只能调用它的常成员函数,而不能调用其他成员函数。

42. const常成员函数、virtual虚函数、static静态成员函数在类体外定义的样式

1.常成员函数:

class x{    ...    T func(T1, T2, ...) const;    ...}T x::func(T1, T2, ...) const {    ...}
  • 只有类的成员函数才能定义为常量成员函数,普通函数不能定义为常量成员函数;
  • 常量成员函数限制对类内数据成员的修改,不管是不是常量数据,在常量成员函数内都不允许修改;
  • 常量成员函数在类体外定义必须加const修饰。

2.虚函数:

class x {    ...    virtual T func(T1, T2, ...);}T x::func(T1, T2, ...) {    ...}
  • 一旦将某个成员函数定义为虚函数,那它在整个继承体系下永远是虚函数,即使在派生类中没有声明为虚函数,它依然是虚函数;
  • 如果定义虚函数的类从其他类派生,它将不影响基类中的同名函数,基类中的同名函数不会被它所影响而成为虚函数;
  • 如果基类中定义了虚函数,只能通过指针或引用方式调用派生类对象时实现虚函数,访问到实际所指对象的虚函数版本,通过普通类对象访问派生类对象时,不能实现虚函数特征;
  • 虚函数在类体外定义时,不能用virtual限定符限定;

3.静态成员函数:

class x {    ...    static T func(T1, T2, ...);    ...}T x::func(T1, T2, ...) {    ...}
  • 静态成员函数只能访问静态数据成员,不能访问非静态数据成员;
  • 在类体外定义静态函数时,不能用static限定符限定;
  • 静态成员函数可以在定义类的对象之前定义;

43. 虚析构函数

虚析构函数可以防止内存泄漏,保证完全析构掉整个继承体系里的所有内容。

#include <iostream>using namespace std;class P {public:    P() { cout << "调用基类构造函数" << endl; }    virtual ~P() { cout << "调用基类析构函数" << endl; }       //注释A};class Q :public P {private:    int *a = new int;public:    Q() { cout << "调用派生类构造函数" << endl; }    ~Q() { delete a; cout << "调用派生类析构函数" << endl; }};int main(){    P *s = new Q;    delete s;    return 0;}

如果注释A行中,将virtual取消,则会引发派生类中a的内存没有被释放,因为没有调用派生类的析构函数。

44. 重载new和delete运算符

void * classname::operator new(size_t size, <arg>) {...}void classname::operator delete(void *p, <size_t size>) {...}
  • 重载运算符new和delete的函数必须是成员函数。
  • 重载运算符new和delete的函数总是静态的成员函数,在operator new和operator delete的定义中,不管是否用到了static关键词,编译时总把这两个重载函数看成是静态的成员函数。
  • operator new和operator delete函数不能是虚函数。
1 0