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