关于C++的问题

本文详细讨论了C++编程中的多个常见问题,包括野指针、delete操作的限制、函数默认参数、名字粉碎机制、重载函数的二义性、循环语句的作用域规则以及构造函数和析构函数的调用时机。此外,还涵盖了类中的struct使用、常量成员函数、静态成员的管理、派生类与基类构造函数的关系、多继承下的二义性问题和虚函数的使用等核心概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

关于C++的问题

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


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

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

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;
}

程序的输出结果是:

1
99

从程序上看,两次都输出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;}       //A
private:
    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,不会调用复制构造函数,如果显式的重载了赋值运算符函数,则会调用这个自定义的赋值运算符函数来完成赋值工作,否则将会调用系统默认赋值运算符来完成赋值工作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值