实例解析 C/C++ 疑难问题(一)
2011-08-07 16:11
316 查看
内联函数的定义方法
定义内联函数的方法很简单,只要在函数定义的头前加上关键字inline即可。内联函数的定义方法与一般函数一样。如:
在程序中,调用其函数时,该函数在编译时被替代,而不是像一般函数那样是在运行时被调用。
使用内联函数应注意的事项
内联函数具有一般函数的特性,它与一般函数所不同之处公在于函数调用的处理。一般函数进行调用时,要将程序执行权转到被调用函数中,然后再返回到调用它的函数中;而内联函数在调用时,是将调用表达式用内联函数体来替换。在使用内联函数时,应注意如下几点:
1.类内定义的函数是内联函数,类外定义的函数是非内联函数(短函数可以定义在类内,长函数可以定义在类外)。
2.可以为类外定义的函数指定 inline 关键字,强行为内联函数。
3.在内联函数内不允许用循环语句和开关语句。
4.内联函数的定义必须出现在内联函数第一次被调用之前。
2.内联函数的定义必须出现在内联函数第一次被调用之前。
3.本栏目讲到的类结构中所有在类说明内部定义的函数是内联函数。
1.动态内存的传递:
# include <iostream>
void GetMemory(char *p,int num)
{
p=(char*)malloc(sizeof(char)*num);
}
int main()
{
char *str=NULL;
GetMemory(str,100);
strcpy(str,"hello");
return 0;
}
总结:指针当参数传进去的是它的一个副本,指针str本身还是NULL,str并不指向指针P指向的那段内存。函数GetMemory没有返回值,故函数退出后,指针副本消失,这样会造成内存泄露。
2.局部数组和全局数组
char c[] ="hello world";
char* c="hello world";
总结:前者可以更改内容,后者不可更改。
前者先在内存的栈区分配数组,然后赋值;后者先在全局区域分配字符串常量的内存,然后将字符串常量的地址赋值给指针c。
3.下列程序会在哪行出现错误:
struct S
{
int i;
int *p
}
void main()
{
S s;
int *p=&s.i;
p[0]=4;
p[1]=3;
s.p=p;
s.p[1]=1;
s.p[0]=2;//会出现错误。
//p[2]=7;//运行到这里也会出错,因为超出结构体的内存范围,对于一个未说明的地址直接访问会出错
}
总结:程序运行到 s.p[0]=2;会出现错误。首先明白在结构体里面,指针P在i的接下来的4个字节的内存位置。
s.p=p;相当于s.p存了p的值,即&s.i,当执行p[0]=4;p[1]=3;的时候,p的值始终是&s.i。
s.p[1]相当于&s.i+1,即s.p在结构体中的内存位置。
所以 s.p[1]=1将0x00000001写入s.p空间
s.p[0]=2,相当于对一个未作声明的地址直接进行写访问。
4.数组指针与指针数组
int (*a)[10];
a++;
这是定义一个指向10个元素的数组的指针。即数组指针
a++指向第11个元素(如果数组有第11个元素的话)。
数组名本身就是一个指针,再加一个&就是双指针。
#include <iostream>
#include<stdio.h>
int main()
{
int v[2][10]={{1,2,3,4,5,6,7,8,9,10},{11,12,13,14,15,16,17,18,19,20}};
int (*a)[10]=v;//数组指针
cout<<**a<<endl;
a++;
cout<<**a<<endl;
return 0;
}
程序输出1, 11。数组指针就是一个二级指针。
main()
{
inta[5]={1,2,3,4,5};
int *ptr=(int *)(&a+1);
printf("%d,%d",*(a+1),*(ptr-1));
}
答案:2。5
*(a+1)就是a[1],*(ptr-1)就是a[4],执行结果是2,5
&a+1不是首地址+1,系统会认为加一个a数组的偏移,是偏移了一个数组的大小(本例是5个int)
int *ptr=(int *)(&a+1);
ptr实际是&(a[5]),也就是a+5
原因如下:&a是数组指针,其类型为 int (*)[5];而指针加1要根据指针类型加上一定的值,不同类型的指针+1之后增加的大小不同,a是长度为5的int数组指针,所以要加
5*sizeof(int),所以ptr实际是a[5],但是prt与(&a+1)类型是不一样的(这点很重要),所以prt-1只会减去sizeof(int*),a,&a的地址是一样的,但意思不一样,a是数组首地址,也就是a[0]的地址,&a是对象(数组)首地址,a+1是数组下一元素的地址,即a[1],&a+1是下一个对象的地址,即a[5].
5.句柄与指针的区别和联系
句柄是一个指向指针的指针,我们知道Windows是一个以虚拟内存为基础的操作系统,在这种系统环境下,内存管理器经常在内存中来回移动对象,以满足各种应用程序的需求。对象被移动,意味着他的地址也跟着变化。如果地址总是变化,我们到哪里去寻找这个对象,为此Windows专门腾出一块内存地址,用来专门登记各应用对象在内存中的地址变化,而这个存储地址本身是不变化的,内存管理器将对象新的地址告诉这个句柄地址来保存。这个地址是对象装载时由系统分配的。
6. for循环语句
for(表达式1;表达式2;表达式3)语句;
这里边的“语句”就是循环体语句,若其中只有一条语句,可以不用花括号;若多于一条,则必须用花括号将这些循环体语句括起来。
(1)先操作表达式1;
(2)操作表达式2,若其值为真(值为非O),则执行for
语句中的循环体语句一次,然后执行下面第三步,若为假
(值为O),则结束循环,转到第5步;
(3)操作表达式3;
(4)转回上面第2步骤继续执行;
(5)结束循环,执行for语句下面的语句。
7.C++虚析构函数调用问题
例1:
我们知道,用C++开发的时候,用来做基类的类的析构函数一般都是虚函数。可是,为什么要这样做呢?下面用一个小例子来说明:
有下面的两个类:
class ClxBase
{
public:
ClxBase() {};
virtual ~ClxBase() {};
virtual void DoSomething() { cout << "Do something in class ClxBase!" << endl; };
};
class ClxDerived : public ClxBase
{
public:
ClxDerived() {};
~ClxDerived() { cout << "Output from the destructor of class ClxDerived!" << endl; };
void DoSomething() { cout << "Do something in class ClxDerived!" << endl; };
};
代码
ClxBase *pTest = new ClxDerived;
pTest->DoSomething();
delete pTest;
的输出结果是:
Do something in class ClxDerived!
Output from the destructor of class ClxDerived!
这个很简单,非常好理解。
但是,如果把类ClxBase析构函数前的virtual去掉,那输出结果就是下面的样子了:
Do something in class ClxDerived!
也就是说,类ClxDerived的析构函数根本没有被调用!一般情况下类的析构函数里面都是释放内存资源,而析构函数不被调用的话就会造成内存泄漏。我想所有的C++程序员都知道这样的危险性。当然,如果在析构函数中做了其他工作的话,那你的所有努力也都是白费力气。
所以,文章开头的那个问题的答案就是--这样做是为了当用一个基类的指针删除一个派生类的对象时,派生类的析构函数会被调用。
当然,并不是要把所有类的析构函数都写成虚函数。因为当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里面来存放虚函数指针,这样就会增加类的存储空间。所以,只有当一个类被用来作为基类的时候,才把析构函数写成虚函数。
如果继承是多态的方式,则一定要将基类的析构函数设置为virtual形式的。
例2:
#include "stdafx.h"
#include <iostream>
using namespace std;
class Base
{
public:
virtual ~Base(){cout<<"~Base"<<endl;};
};
class Derived:public Base
{
public:
~Derived(){cout<<"~Derived"<<endl;}
};
int _tmain(int argc, _TCHAR* argv[])
{
//char *p="google";
//char pp[]="abcdefg";
//cout<<pp[1];
Base *p=new Derived;
//p->~Base();
delete p;
system("Pause");
return 0;
}
输出:~Derived ~Base,如果去掉virtual 则输出为:~Base;基类析构函数声明为虚函数时,就是动态绑定;否则就是静态绑定。
不管什么情况下,类的实例都会调用析构函数,没有自定义的,就用默认的,默认的析构函数可以清除类变量,如string之类(自带构造和析构函数的类)的变量,如要清除指向对象的指针,一定要自定义的析构函数。
8.拷贝构造函数(浅拷贝和深拷贝)
深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。
在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。
#include <iostream>
using namespace std;
class CA
{
public:
CA(int b,char* cstr)
{
a=b;
str=new char;
strcpy(str,cstr);
}
CA(const CA& C)
{
a=C.a;
str=new char[a]; //深拷贝
if(str!=0)
strcpy(str,C.str);
}
void Show()
{
cout<<str<<endl;
}
~CA()
{
delete str;
}
private:
int a;
char *str;
};
int main()
{
CA A(10,"Hello!");
CA B=A;
B.Show();
return 0;
}
深拷贝和浅拷贝的定义可以简单理解成:如果一个类拥有资源(堆,或者是其它系统资源),当这个类的对象发生复制过程的时候,这个过程就可以叫做深拷贝,反之对象存在资源,但复制过程并未复制资源的情况视为浅拷贝。
浅拷贝资源后在释放资源的时候会产生资源归属不清的情况导致程序运行出错。
Test(Test &c_t)是自定义的拷贝构造函数,拷贝构造函数的名称必须与类名称一致,函数的形式参数是本类型的一个引用变量,且必须是引用。
当用一个已经初始化过了的自定义类类型对象去初始化另一个新构造的对象的时候,拷贝构造函数就会被自动调用,如果你没有自定义拷贝构造函数的时候,系统将会提供给一个默认的拷贝构造函数来完成这个过程,上面代码的复制核心语句就是通过Test(Test &c_t)拷贝构造函数内的p1=c_t.p1;语句完成的。
9.深层揭秘 extern "C"
实现C++与C及其它语言的混合编程。
被extern "C"限定的函数或变量是extern类型的;extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。
(1)在C++中引用C语言中的函数和变量,在包含C语言头文件(假设为cExample.h)时,需进行下列处理:
extern "C"
{
#include "cExample.h"
}
而在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern "C"声明,在.c文件中包含了extern "C"时会出现编译语法错误。
笔者编写的C++引用C函数例子工程中包含的三个文件的源代码如下:
/* c语言头文件:cExample.h */
#ifndef C_EXAMPLE_H
#define C_EXAMPLE_H
extern int add(int x,int y);
#endif
/* c语言实现文件:cExample.c */
#include "cExample.h"
int add( int x, int y )
{
return x + y;
}
// c++实现文件,调用add:cppFile.cpp
extern "C"
{
#include "cExample.h"
}
int main(int argc, char* argv[])
{
add(2,3);
return 0;
}
如果C++调用一个C语言编写的.DLL时,当包括.DLL的头文件或声明接口函数时,应加extern "C" { }。
(2)在C中引用C++语言中的函数和变量时,C++的头文件需添加extern "C",但是在C语言中不能直接引用声明了extern "C"的该头文件,应该仅将C文件中将C++中定义的extern "C"函数声明为extern类型。
笔者编写的C引用C++函数例子工程中包含的三个文件的源代码如下:
//C++头文件 cppExample.h
#ifndef CPP_EXAMPLE_H
#define CPP_EXAMPLE_H
extern "C" int add( int x, int y );
#endif
//C++实现文件 cppExample.cpp
#include "cppExample.h"
int add( int x, int y )
{
return x + y;
}
/* C实现文件 cFile.c
/* 这样会编译出错:#include "cExample.h" */
extern int add( int x, int y );
int main( int argc, char* argv[] )
{
add( 2, 3 );
return 0;
}
10.子类对父类的同名函数的覆盖
如果一个类,存在和父类相同的函数,那么,这个类将会覆盖其父类的方法,除非你在调用的时候,强制转换为父类类型,否则试图对子类和父类做类似重载的调用是不能成功的。
关于函数重定义
class A
{
public:
void fun()
{
printf("A\n");
}
};
class B:public A
{
public:
void fun()
{
printf("B\n");
}
};
int main(int argc, char* argv[])
{
A a;
a.fun();
B b;
b.fun();
b.A::fun();
printf("\n");
return 0;
}
如果基类的函数是 virtual,在派生类里重定义,才会动态绑定,否则就是屏蔽了。这种程序本身就有很大的弊病,作为讨论可以,但真正使用的话,还是抛弃的好。子类尽量不要重新定义继承而来的非虚函数,这会导致“不变性凌驾特异性”的性质(effect
C++)混乱。如果要重写,把父类的相应函数定义为虚函数。子类会继承父类的所有成员,在子类中重定义父类的同名函数后,只是在用子类对象调用该函数是只会执行子类重定义后的函数,如果要调用父类的同名函数则要用::域运算符来调用!
父类虚函数,子类重新定义,但前面没有virtual关键字
#include
using namespace std;
class Parent{
public:
void virtual foo(){
cout << "A" << endl;
}
};
class Son:public Parent{
public:
//形成覆盖,子类重新定义父类的虚函数
void foo(){
cout << "foo from son" << endl;
}
};
int main(){
Parent *pa = new Parent();
pa->foo();
Son* pb = (Son*)pa;
pb->foo();
delete pa,pb;
pa = new Son();
pa->foo();
pb = (Son*)pa;
pb->foo();
return 0;
}
输出 :AABB
10 .[b]类成员函数的重载、覆盖和隐藏区别?
答案:
a.成员函数被重载的特征:
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。
b.覆盖是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。(子类的函数可以没有关键字virtual,也形成对父类函数的覆盖)
c.“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)
11.类成员变量和函数的地址
记住:函数名字本身就是一个指针
做下面的一个测试
view
plainprint?
#include<iostream>
using namespace std;
class A
{
public:
A(int);
void fun1();
virtual void fun2();
static void fun3();
int num1;
static int num2;
};
A::A(int i)
{
num1=i;
}
void A::fun1()
{
cout<<"I am in fun1"<<endl;
}
void A::fun2()
{
cout<<"I am in fun2"<<endl;
}
void A::fun3()
{
cout<<"I am in fun3"<<endl;
}
int A::num2=1;
void main()
{
A a(2);
//获取静态成员数据的地址
int *ptr_static=&A::num2;
cout<<"静态成员数据的地址"<<ptr_static<<endl;
ptr_static=&a.num2;
cout<<"a.num2静态成员数据的地址"<<ptr_static<<endl;
//获取静态函数的地址
void (*ptr_staticfun)=A::fun3;
cout<<"静态成员函数的地址"<<ptr_staticfun<<endl;
ptr_staticfun=a.fun3;
cout<<"a.fun3静态成员函数的地址"<<ptr_staticfun<<endl;
//获取普通成员函数的地址
typedef void (A::*ptr_commomfun)();
函数指针类型声明。
ptr_commomfun ptr=A::fun1;
//函数指针类型实例
cout<<"普通成员函数的地址"<<ptr<<endl; //如果直接输出ptr的话,输出来的是1,因此应该把ptr地址中的内容读出来
cout<<"普通成员函数的地址"<<*((long*)&ptr)<<endl;
ptr=a.fun1;
cout<<"a.fun1普通成员函数的地址"<<*((long*)&ptr)<<endl;
ptr_commomfun ptr_virtual=A::fun2; //获取虚函数的地址
cout<<"虚成员函数的地址"<<*((long*)&ptr_virtual)<<endl;
ptr_virtual=a.fun2;
cout<<"a.fun2虚成员函数的地址"<<*((long*)&ptr_virtual)<<endl;
int *p;
int A::*q;
q=&A::num1;
//指向数据成员的指针赋予的是一个目前还不存在的一个类成员的地址,而这个地址只有在使用实际类对象进行调用时才会真正的确定下来
//就是说在类还没有对象时候,成员变量时没有空间的。
cout<<"普通成员数据的地址"<<*((long *)&q)<<endl;
p=&a.num1;
cout<<"a.num2普通成员数据的地址"<<p<<endl;
}
注意在获取类成员函数的时候,如果直接把指针输出来,得到的是1,我想是因为编译器把&A::fun1当做bool变量
void (A::*ptr)();
ptr=A::fun1;或者ptr=&A::fun1都可以
另外需要注意的是
指向数据成员的指针赋予的是一个目前还不存在的一个类成员的地址,而这个地址只有在使用实际类对象进行调用时才会真正的确定下来
就是说在类还没有对象时候,成员变量时没有空间的
运行结果
12.函数指针、函数指针类型、函数类型
定义:函数指针是指指向函数的指针。像其他指针一样,函数指针也指向特定的类型。函数类型由其返回值以及形参表确定,而与函数名无关。
e.g
void (*pf) ( char,int );
这个语句将pf声明指向函数的指针,它所指向的函数带有一个char类型,一个int类型,返回类型为void
我们可以这样理解:我们怎么定义普通的指针呢,如我们定义一个int型的指针,
int
*p;
是在变量声明前面加*,即p前面加上*号。而我们定义函数指针要在函数声明前加*, 函数声明为
void
pf( char,int );
函数声明前加*后变成
void
*pf(char,int);
我们把*pf用小括号括起来,变成
void (*pf) ( char,int ); 这就是函数指针的声明方法
测试代码如下:
#include"stdio.h"
void (*pf)(char, int);
void fun(char ,int); //声明一个函数,形参为一个char类型,一个int类型,返回类型为void
int main()
{
pf=fun; //给函数指针pf赋值为fun函数的地址(函数名代表函数的地址)
(*pf)('c',90); //调用pf指向的函数
}
void fun(char a,int b)
{
printf("the argument is %c and %d\n",a,b);
}
函数运行后的结果是
The argumeng is c and 90
函数指针类型相当地冗长。使用typedef为指针类型定义同义词,可将函数指针的使用大大简化
typedef void (*FCN) (char,int);
记忆方法:在函数指针声明 void (*FCN)(char,int)前加上typedef关键字就是函数指针类型的声明。
该定义表示FCN是一种函数指针类型。该函数指针类型表示这样一类函数指针:
指向返回void类型并带有一个char类型,一个int类型的函数指针。
测试代码如下:
#include"stdio.h"
typedef void (*FCN)(char, int); //声明一个函数指针类型
void fun(char ,int); //声明一个函数,形参为一个char类型,一个int类型,返回类型为void
int main()
{
FCN pf;
pf=fun; //给函数指针pf赋值为fun函数的地址(函数名代表函数的地址)
(*pf)('c',90); //调用pf指向的函数
}
void fun(char a,int b)
{
printf("the argument is %c and %d\n",a,b);
}
函数类型的定义:
typedef void (*FCN)(char, int); //声明一个函数类型
该声明定义了一个函数类型,FCN表示这样一类函数,带有两个形参,一个是int ,一个是char,返回值是void型。一般用于函数声明和函数的形参。
一般我们在调用函数时,应该先声明要调用函数,如我们调用fun函数,则应在调用的前面声明void fun(char ,int);
如果我们定义了函数类型typedef void FCN(char , int);我们就可以这样声明函数原形,
FCN fun;
大大简化了函数原型的声明,函数类型用于形参的情况我们在下面讲解。
#include"stdio.h"
typedef void FCN(char , int);
int main()
{ FCN fun;
fun('c',90);
}
void fun(char a,int b)
{
printf("the argument is %c and %d\n",a,b);
}
13.C++三类继承方式
暂不考虑继承:
对类成员访问权限的控制,是通过设置成员的访问控制属性实现的。访问控制属性有以下三种:public,private和protected。
public成员:
任何一个来自类外部的访问都必须通过这种类型的成员来访问(“对象.公有成员”)。公有类型声明了类的外部接口。
private成员:
(若私有类型成员紧接着类名称,可省略关键字),私有类型的成员只允许本类的成员函数来访问,而类外部的任何访问都是非法的。这样完成了私有成员的隐蔽。
protected成员:
性质和私有类型的性质一致。即保护类型和私有类型的性质相似,其差别在于继承过程中对产生的新类影响不同。
C++中的继承方式有:
public、private、protected三种(它们直接影响到派生类的成员、及其对象对基类成员访问的规则)。
(1)public(公有继承):继承时保持基类中各成员属性不变,并且基类中private成员被隐藏。派生类的成员只能访问基类中的public/protected成员,而不能访问private成员;派生类的对象只能访问基类中的public成员。
(2)private(私有继承):继承时基类中各成员属性均变为private,并且基类中private成员被隐藏。派生类的成员也只能访问基类中的public/protected成员,而不能访问private成员;派生类的对象不能访问基类中的任何的成员。
(3)protected(保护性继承):继承时基类中各成员属性均变为protected,并且基类中private成员被隐藏。派生类的成员只能访问基类中的public/protected成员,而不能访问private成员;派生类的对象不能访问基类中的任何的成员。
14.析构函数
不管什么情况下,类的实例都会调用析构函数,没有自定义的,就用默认的,默认的析构函数可以清除类变量,如string之类(自带构造和析构函数的类)的变量,如要清除指向对象的指针,一定要自定义的析构函数。
15 继承与组合
若在逻辑上,A是B的一部分,则不许B从A继承,而是要用A和其他东西组合出B。
例如 眼,鼻子,耳朵是头的一部分,所以头应该有前三者组合而成,而不应该继承前三者。
如果头继承 眼睛、鼻子、耳朵等,则自动具有看、闻、听等功能(依据构父类和子类的构造函数的构造顺序)。
16 联合体union内存占用问题
structA
{
int o;
int j;
union
{
int i[10],j,k;
};
};
sizeof(A) //48
#pragma pack(1)
struct A
{
enum day{monring, moon, aftermoon};
};
sizeof(A) //1,结构体类型占用一个字节;
sizeof(A::day) //4,枚举成员占4个字节;
在C/C++程序的编写中,当多个基本数据类型或复合数据结构要占用同一片内存时,我们要使用联合体;当多种类型,多个对象,多个事物只取其一时(我们姑且通俗地称其为“n 选1”),我们也
可以使用联合体来发挥其长处。首先看一段代码:
union myun
{
struct { int x; int y; int z; }u;
int k;
}a;
int main()
{
a.u.x =4;
a.u.y =5;
a.u.z =6;
a.k = 0;
printf("%d %d %d\n",a.u.x,a.u.y,a.u.z);
return 0;
}
union类型是共享内存的,以size最大的结构作为自己的大小,这样的话,myun这个结构就包含u这个结构体,而大小也等于u这个结构体 的大小,在内存中的排列为声明的顺序x,y,z从低到高,然后赋值的时候,在内存中,就是x的位置放置4,y的位置放置5,z的位置放置6,现在对k赋 值,对k的赋值因为是union,要共享内存,所以从union的首地址开始放置,首地址开始的位置其实是x的位置,这样原来内存中x的位置就被k所赋的 值代替了,就变为0了,这个时候要进行打印,就直接看内存里就行了,x的位置也就是k的位置是0,而y,z的位置的值没有改变,所以应该是0,5,6
#i nclude <stdio.h>
union
{
int i;
charx[2];
}a;
voidmain()
{
a.x[0]= 10;
a.x[1]= 1;
printf("%d",a.i);
}
答案:266 (低位低地址,高位高地址,内存占用情况是Ox010A)
b)
main()
{
union{ /*定义一个联合*/
int i;
struct{ /*在联合中定义一个结构*/
char first;
char second;
}half;
}number;
number.i=0x4241; /*联合成员赋值*/
printf("%c%c\n",number.half.first, mumber.half.second);
number.half.first='a'; /*联合中结构成员赋值*/
number.half.second='b';
printf("%x\n", number.i);//按照十六进制输出
getch();
}
答案: AB (0x41对应'A',是低位;Ox42对应'B',是高位)//字符: 左 到 右 ——> 低 到 高
6261 (number.i和number.half共用一块地址空间)//数字: 右 到 左 ——> 高 到 低
备注:十六进制:A 41 B42
a 61 b 62
十进制: A 65 B 66
a 97 b 98
运算符sizeof可以计算出给定类型的大小,对于32位系统来说,sizeof(char) = 1; sizeof(int) = 4。基本数据类型的大小很好计算,我们来看一下如何计算构造数据类型的大小。
C语言中的构造数据类型有三种:数组、结构体和共用体。
数组是相同类型的元素的集合,只要会计算单个元素的大小,整个数组所占空间等于基础元素大小乘上元素的个数。
结构体中的成员可以是不同的数据类型,成员按照定义时的顺序依次存储在连续的内存空间。和数组不一样的是,结构体的大小不是所有成员大小简单的相加,需要考虑到系统在存储结构体变量时的地址对齐问题。看下面这样的一个结构体:
struct stu1
{
int i;
char c;
int j;
};
先介绍一个相关的概念——偏移量。偏移量指的是结构体变量中成员的地址和结构体变量地址的差。结构体大小等于最后一个成员的偏移量加上最后一个成员的大小。显然,结构体变量中第一个成员的地址就是结构体变量的首地址。因此,第一个成员i的偏移量为0。第二个成员c的偏移量是第一个成员的偏移量加上第一个成员的大小(0+4),其值为4;第三个成员j的偏移量是第二个成员的偏移量加上第二个成员的大小(4+1),其值为5。
实际上,由于存储变量时地址对齐的要求,编译器在编译程序时会遵循两条原则:一、结构体变量中成员的偏移量必须是成员大小的整数倍(0被认为是任何数的整数倍) 二、结构体大小必须是所有成员大小的整数倍。
对照第一条,上面的例子中前两个成员的偏移量都满足要求,但第三个成员的偏移量为5,并不是自身(int)大小的整数倍。编译器在处理时会在第二个成员后面补上3个空字节,使得第三个成员的偏移量变成8。
对照第二条,结构体大小等于最后一个成员的偏移量加上其大小,上面的例子中计算出来的大小为12,满足要求。
再看一个满足第一条,不满足第二条的情况
struct stu2
{
int k;
shortt;
};
成员k的偏移量为0;成员t的偏移量为4,都不需要调整。但计算出来的大小为6,显然不是成员k大小的整数倍。因此,编译器会在成员t后面补上2个字节,使得结构体的大小变成8从而满足第二个要求。由此可见,大家在定义结构体类型时需要考虑到字节对齐的情况,不同的顺序会影响到结构体的大小。对比下面两种定义顺序
struct stu3
{
char c1;
int i;
char c2;
}
struct stu4
{
char c1;
char c2;
int i;
}
虽然结构体stu3和stu4中成员都一样,但sizeof(struct stu3)的值为12而sizeof(struct stu4)的值为8。
如果结构体中的成员又是另外一种结构体类型时应该怎么计算呢?只需把其展开即可。但有一点需要注意,展开后的结构体的第一个成员的偏移量应当是被展开的结构体中最大的成员的整数倍。看下面的例子:
struct stu5
{
short i;
struct
{
char c;
int j;
} ss;
int k;
}
结构体stu5的成员ss.c的偏移量应该是4,而不是2。整个结构体大小应该是16。
如何给结构体变量分配空间由编译器决定,以上情况针对的是Linux下的GCC。其他平台的C编译器可能会有不同的处理。
17.关于运算符&& || 的面试题
bool Fun1(char* str)
{
printf("%s\n",str);
return false;
}
bool Fun2(char* str)
{
printf("%s\n",str);
return true;
}
int _tmain(int argc, _TCHAR* argv[])
{
bool res1,res2;
res1 = (Fun1("a")&& Fun2("b")) || (Fun1("c") || Fun2("d"));
res2 = (Fun1("a")&& Fun2("b")) &&(Fun1("c") || Fun2("d"));
return res1|| res2;
}
答案:打印出4行,分别是a、c、d、a。
在C/C++中,与、或运算是从左到右的顺序执行的。在计算rest1时,先计算Fun1(“a”)&& Func2(“b”)。首先Func1(“a”)打印出内容为a的一行。由于Fun1(“a”)返回的是false,无论Func2(“b”)的返回值是true还是false,Fun1(“a”)&& Func2(“b”)的结果都是false。由于Func2(“b”)的结果无关重要,因此Func2(“b”)会略去而不做计算。接下来计算Fun1(“c”)|| Func2(“d”),分别打印出内容c和d的两行。
在计算rest2时,首先Func1(“a”)打印出内容为a的一行。由于Func1(“a”)返回false,和前面一样的道理,Func2(“b”)会略去不做计算。由于Fun1(“a”)&& Func2(“b”)的结果是false,不管Fun1(“c”)&& Func2(“d”)的结果是什么,整个表达式得到的结果都是false,因此Fun1(“c”) || Func2(“d”)都将被忽略。
18.虚函数调用肯定是从虚函数表中调用,子类对父类形成覆盖
问题(25):运行下面的C++代码,打印的结果是什么?
13。关键字volatile有什么含意?并举出三个不同的例子?
答案:提示编译器对象的值可能在编译器未监测到的情况下改变。
14。int (*s[10])(int) 表示的是什么啊?
答案:int(*s[10])(int) 函数指针数组,每个指针指向一个int func(int param)的函数。
答案:输出两行,分别是Base::doPrint和Derived::doPrint。在print中调用doPrint时,doPrint()的写法和this->doPrint()是等价的,因此将根据实际的类型调用对应的doPrint。所以结果是分别调用的是Base::doPrint和Derived::doPrint2。如果感兴趣,可以查看一下汇编代码,就能看出来调用doPrint是从虚函数表中得到函数地址的。
19.C/C++ 语言中的表达式求值
问题一:经常可以在一些讨论组里看到下面的提问:“谁知道下面C语句给n赋什么值?”
m = 1; n = m+++m++;
问题二:问为什么在某个C++系统里,下面表达式打印出两个4,而不是4和5:
a = 4; cout << a++ << a;
要弄清这些,需要理解的一个问题是:如果程序里某处修改了一个变量(通过赋值、增量/减量操作等),什么时候从该变量能够取到新值?有人可能说,“这算什么问题!我修改了变量,再从这个变量取值,取到的当然是修改后的值!”其实事情并不这么简单。
C/C++ 语言是“基于表达式的语言”,所有计算(包括赋值)都在表达式里完成。“x = 1;”就是表达式“x = 1”后加表示语句结束的分号。要弄清程序的意义,首先要理解表达式的意义,也就是:1)表达式所确定的计算过程;2)它对环境(可以把环境看作当时可用的所有变量)的影响。如果一个表达式(或子表达式)只计算出值而不改变环境,我们就说它是引用透明的,这种表达式早算晚算对其他计算没有影响(不改变计算的环境。当然,它的值可能受到其他计算的影响)。如果一个表达式不仅算出一个值,还修改了环境,就说这个表达式有副作用(因为它多做了额外的事)。a++
就是有副作用的表达式。这些说法也适用于其他语言里的类似问题。
我们假定程序里有代码片段“...a[i]++ ... a[j] ...”,假定当时i与j的值恰好相等(a[i] 和a[j] 正好引用同一数组元素);假定a[i]++ 确实在a[j] 之前计算;再假定其间没有其他修改a[i] 的动作。在这些假定下,a[i]++ 对 a[i] 的修改能反映到 a[j] 的求值中吗?注意:由于 i 与 j 相等的问题无法静态判定,在目标代码里,这两个数组元素访问(对内存的访问)必然通过两段独立代码完成。现代计算机的计算都在寄存器里做,问题现在变成:在取 a[j] 值的代码执行之前,a[i]
更新的值是否已经被(从寄存器)保存到内存?如果了解语言在这方面的规定,这个问题的答案就清楚了。
程序语言通常都规定了执行中变量修改的最晚实现时刻(称为顺序点、序点或执行点)。程序执行中存在一系列顺序点(时刻),语言保证一旦执行到达一个顺序点,在此之前发生的所有修改(副作用)都必须实现(必须反应到随后对同一存储位置的访问中),在此之后的所有修改都还没有发生。在顺序点之间则没有任何保证。对C/C++ 语言这类允许表达式有副作用的语言,顺序点的概念特别重要。
现在上面问题的回答已经很清楚了:如果在a[i]++ 和a[j] 之间存在一个顺序点,那么就能保证a[j] 将取得修改之后的值;否则就不能保证。
C/C++语言定义(语言的参考手册)明确定义了顺序点的概念。顺序点位于:
1. 每个完整表达式结束时。完整表达式包括变量初始化表达式,表达式语句,return语句的表达式,以及条件、循环和switch语句的控制表达式(for头部有三个控制表达式);
2. 运算符 &&、||、?: 和逗号运算符的第一个运算对象计算之后;
3. 函数调用中对所有实际参数和函数名表达式(需要调用的函数也可能通过表达式描述)的求值完成之后(进入函数体之前)。
假设时刻ti和ti+1是前后相继的两个顺序点,到了ti+1,任何C/C++ 系统(VC、BC等都是C/C++系统)都必须实现ti之后发生的所有副作用。当然它们也可以不等到时刻ti+1,完全可以选择在时段 [t, ti+1] 之间的任何时刻实现在此期间出现的副作用,因为C/C++ 语言允许这些选择。
前面讨论中假定了a[i]++ 在a[i] 之前做。在一个程序片段里a[i]++ 究竟是否先做,还与它所在的表达式确定的计算过程有关。我们都熟悉C/C++ 语言有关优先级、结合性和括号的规定,而出现多个运算对象时的计算顺序却常常被人们忽略。看下面例子:
(a + b) * (c + d) fun(a++, b, a+5)
这里“*”的两个运算对象中哪个先算?fun及其三个参数按什么顺序计算?对第一个表达式,采用任何计算顺序都没关系,因为其中的子表达式都是引用透明的。第二个例子里的实参表达式出现了副作用,计算顺序就非常重要了。少数语言明确规定了运算对象的计算顺序(Java规定从左到右),C/C++ 则有意不予规定,既没有规定大多数二元运算的两个对象的计算顺序(除了&&、|| 和 ,),也没有规定函数参数和被调函数的计算顺序。在计算第二个表达式时,首先按照某种顺序算fun、a++、b和a+5,之后是顺序点,而后进入函数执行。
不少书籍在这些问题上有错(包括一些很流行的书)。例如说C/C++ 先算左边(或右边),或者说某个C/C++ 系统先计算某一边。这些说法都是错误的!一个C/C++ 系统可以永远先算左边或永远先算右边,也可以有时先算左边有时先算右边,或在同一表达式里有时先算左边有时先算右边。不同系统可能采用不同的顺序(因为都符合语言标准);同一系统的不同版本完全可以采用不同方式;同一版本在不同优化方式下,在不同位置都可能采用不同顺序。因为这些做法都符合语言规范。在这里还要注意顺序点的问题:即使某一边的表达式先算了,其副作用也可能没有反映到内存,因此对另一边的计算没有影响。
回到前面的例子:“谁知道下面C语句给n赋什么值?”
m = 1; n = m++ +m++;
正确回答是:不知道!语言没有规定它应该算出什么,结果完全依赖具体系统在具体上下文中的具体处理。其中牵涉到运算对象的求值顺序和变量修改的实现时刻问题。对于:
cout << a++ << a;
我们知道它是
(cout.operator <<(a++)).operator << (a);
的简写。先看外层函数调用,这里需要算出所用函数(由加下划线的一段得到),还需要计算a的值。语言没有规定哪个先算。如果真的先算函数,这一计算中出现了另一次函数调用,在被调函数体执行前有一个顺序点,那时a++的副作用就会实现。如果是先算参数,求出a的值4,而后计算函数时的副作用当然不会改变它(这种情况下输出两个4)。当然,这些只是假设,实际应该说的是:这种东西根本不该写,讨论其效果没有意义。
有人可能说,为什么人们设计 C/C++时不把顺序规定清楚,免去这些麻烦?C/C++ 语言的做法完全是有意而为,其目的就是允许编译器采用任何求值顺序,使编译器在优化中可以根据需要调整实现表达式求值的指令序列,以得到效率更高的代码。像Java那样严格规定表达式的求值顺序和效果,不仅限制了语言的实现方式,还要求更频繁的内存访问(以实现副作用),这些可能带来可观的效率损失。应该说,在这个问题上,C/C++和Java的选择都贯彻了它们各自的设计原则,各有所获(C/C++ 潜在的效率,Java更清晰的程序行为),当然也都有所失。还应该指出,大部分程序设计语言实际上都采用了类似C/C++的规定。
讨论了这么多,应该得到什么结论呢?C/C++ 语言的规定告诉我们,任何依赖于特定计算顺序、依赖于在顺序点之间实现修改效果的表达式,其结果都没有保证。程序设计中应该贯彻的规则是:如果在任何“完整表达式”(形成一段由顺序点结束的计算)里存在对同一“变量”的多个引用,那么表达式里就不应该出现对这一“变量”的副作用。否则就不能保证得到预期结果。注意:这里的问题不是在某个系统里试一试的问题,因为我们不可能试验所有可能的表达式组合形式以及所有可能的上下文。这里讨论的是语言,而不是某个实现。总而言之,绝不要写这种表达式,否则我们或早或晚会某种环境中遇到麻烦。
在封装中C++类数据成员大多情况是private属性;但是如果接口采用多参数实现肯定影响程序效率;然而这时候如果外界需要频繁访问这些私有成员,就不得不需要一个既安全又理想的“后门”——友元关系;
C++中提供三种友元关系的实现方式,友元函数、友元成员函数、友元类。
友元函数:既将一个普通的函数在一个类中说明为一个friend属性;其定义(大多数会访问该类的成员)应在类后;
友元成员函数:既然是成员函数,那么肯定这个函数属于某个类,对了就是因为这个函数是另外一个类的成员函数,有时候因为我们想用一个类通过一个接口去访问另外一个类的信息,然而这个信息只能是被它授权的类才能访问;那么也需要用friend去实现;这个概念只是在声明的时候稍有变化;
友元类:友元类声明会将整个类说明成为另一个类的友元关系;和之前两种的区别是集体和个人的区别;友元类的所有成员函数都可以是另一个类的友元函数;
值得注意的是友元关系是单向的,有点像我们恋爱中出现的单相思 O(∩_∩)O,单向关系就是说如果A被说明成B的友元关系,那么只能说A是B的友元,并不代表B是A的友元;其次在多数情况下友元关系的函数都会访问它被说明中类的成员,这时候应该将函数定义在类的后面;
下面给一个简单的例程代码;
1 #include <iostream>
2
3 using namespace std;
4
5 class B;
6
7 class A{
8 private:
9 int x;
10 public:
11 A();
12 void display(B &);
13 };
14
15 class C;
16
17 class B{
18 private:
19 int y;
20 int z;
21 public:
22 B();
23 B(int, int);
24 friend void A::display(B &);//友元成员函数
25 friend void display(B &);//友元函数
26 friend class C;//友元类
27 };
28
29 class C{
30 private:
31 int sum;
32 void calc(B &);
33 public:
34 C();
35 void display(B &);
36 };
37
38 //必须在友元关系的类后进行定义
39 void display(B &v)//友元函数
40 {
41 cout << v.y << " " << v.z << endl;
42 }
43
44 A::A()
45 {
46 this->x = 0;
47 }
48
49 void A::display(B &v)//友元成员函数
50 {
51 this->x = v.y + v.z;
52 cout << this->x << endl;
53 }
54
55 B::B()
56 {
57 this->y = 0;
58 this->z = 0;
59 }
60
61 B::B(int y, int z)
62 {
63 this->y = y;
64 this->z = z;
65 }
66
67 C::C()
68 {
69 sum = 0;
70 }
71
72 void C::display(B &v)
73 {
74 this->calc(v);
75 cout << sum << " = " << v.y << " + " << v.z << endl;
76 }
77
78 void C::calc(B &v)
79 {
80 sum = v.y + v.z;
81 }
82
83 int main()
84 {
85 A a;
86 B b(2, 3);
87 display(b);
88 a.display(b);
89 C c;
90 c.display(b);
91
92 return 0;
93 }
94
定义内联函数的方法很简单,只要在函数定义的头前加上关键字inline即可。内联函数的定义方法与一般函数一样。如:
inline int add_int (int x, int y, int z) { return x+y+z; } |
使用内联函数应注意的事项
内联函数具有一般函数的特性,它与一般函数所不同之处公在于函数调用的处理。一般函数进行调用时,要将程序执行权转到被调用函数中,然后再返回到调用它的函数中;而内联函数在调用时,是将调用表达式用内联函数体来替换。在使用内联函数时,应注意如下几点:
1.类内定义的函数是内联函数,类外定义的函数是非内联函数(短函数可以定义在类内,长函数可以定义在类外)。
2.可以为类外定义的函数指定 inline 关键字,强行为内联函数。
3.在内联函数内不允许用循环语句和开关语句。
4.内联函数的定义必须出现在内联函数第一次被调用之前。
2.内联函数的定义必须出现在内联函数第一次被调用之前。
3.本栏目讲到的类结构中所有在类说明内部定义的函数是内联函数。
开关语句:
switch语句下面的case后面的序号是不是数字由小到大执行。就像 case2: case1: case0:是不是先执行case0还是由上往下执行。 还有default是不是不管放哪都是最后执行。
第一个个问题是这样的,switch中有值和case后面的值相等的时候,就执行case那行语句,switch都是从上往下判断的,C语言中的语句执行流程就是从下往上(别弄糊涂了),所以switch 执行也是一样的。不是先执行case0,而是先判断switch中的值是否为 2 -> 1 -> 0 从上往下依次判断下来。如果switch里的值为0 的话,就执行case0,好好想下! 第二个问题,说实话我以前没有把default放到case中间编译过(也不知道编译器是否报错),呵呵,虽然这种是无用功,但是对于了解编译还是有帮助的,反正结果应该是这样,执行到default后,下面的case都不会执行。 “default是不是不管放哪都是最后执行”,不是这样的,不管default放到哪儿,顺序由上往下执行到default的时候,它就会执行,尽管你后面还有case语句等等,都忽略了!
1.动态内存的传递:
# include <iostream>
void GetMemory(char *p,int num)
{
p=(char*)malloc(sizeof(char)*num);
}
int main()
{
char *str=NULL;
GetMemory(str,100);
strcpy(str,"hello");
return 0;
}
总结:指针当参数传进去的是它的一个副本,指针str本身还是NULL,str并不指向指针P指向的那段内存。函数GetMemory没有返回值,故函数退出后,指针副本消失,这样会造成内存泄露。
2.局部数组和全局数组
char c[] ="hello world";
char* c="hello world";
总结:前者可以更改内容,后者不可更改。
前者先在内存的栈区分配数组,然后赋值;后者先在全局区域分配字符串常量的内存,然后将字符串常量的地址赋值给指针c。
3.下列程序会在哪行出现错误:
struct S
{
int i;
int *p
}
void main()
{
S s;
int *p=&s.i;
p[0]=4;
p[1]=3;
s.p=p;
s.p[1]=1;
s.p[0]=2;//会出现错误。
//p[2]=7;//运行到这里也会出错,因为超出结构体的内存范围,对于一个未说明的地址直接访问会出错
}
总结:程序运行到 s.p[0]=2;会出现错误。首先明白在结构体里面,指针P在i的接下来的4个字节的内存位置。
s.p=p;相当于s.p存了p的值,即&s.i,当执行p[0]=4;p[1]=3;的时候,p的值始终是&s.i。
s.p[1]相当于&s.i+1,即s.p在结构体中的内存位置。
所以 s.p[1]=1将0x00000001写入s.p空间
s.p[0]=2,相当于对一个未作声明的地址直接进行写访问。
4.数组指针与指针数组
int (*a)[10];
a++;
这是定义一个指向10个元素的数组的指针。即数组指针
a++指向第11个元素(如果数组有第11个元素的话)。
数组名本身就是一个指针,再加一个&就是双指针。
#include <iostream>
#include<stdio.h>
int main()
{
int v[2][10]={{1,2,3,4,5,6,7,8,9,10},{11,12,13,14,15,16,17,18,19,20}};
int (*a)[10]=v;//数组指针
cout<<**a<<endl;
a++;
cout<<**a<<endl;
return 0;
}
程序输出1, 11。数组指针就是一个二级指针。
main()
{
inta[5]={1,2,3,4,5};
int *ptr=(int *)(&a+1);
printf("%d,%d",*(a+1),*(ptr-1));
}
答案:2。5
*(a+1)就是a[1],*(ptr-1)就是a[4],执行结果是2,5
&a+1不是首地址+1,系统会认为加一个a数组的偏移,是偏移了一个数组的大小(本例是5个int)
int *ptr=(int *)(&a+1);
ptr实际是&(a[5]),也就是a+5
原因如下:&a是数组指针,其类型为 int (*)[5];而指针加1要根据指针类型加上一定的值,不同类型的指针+1之后增加的大小不同,a是长度为5的int数组指针,所以要加
5*sizeof(int),所以ptr实际是a[5],但是prt与(&a+1)类型是不一样的(这点很重要),所以prt-1只会减去sizeof(int*),a,&a的地址是一样的,但意思不一样,a是数组首地址,也就是a[0]的地址,&a是对象(数组)首地址,a+1是数组下一元素的地址,即a[1],&a+1是下一个对象的地址,即a[5].
5.句柄与指针的区别和联系
句柄是一个指向指针的指针,我们知道Windows是一个以虚拟内存为基础的操作系统,在这种系统环境下,内存管理器经常在内存中来回移动对象,以满足各种应用程序的需求。对象被移动,意味着他的地址也跟着变化。如果地址总是变化,我们到哪里去寻找这个对象,为此Windows专门腾出一块内存地址,用来专门登记各应用对象在内存中的地址变化,而这个存储地址本身是不变化的,内存管理器将对象新的地址告诉这个句柄地址来保存。这个地址是对象装载时由系统分配的。
6. for循环语句
for(表达式1;表达式2;表达式3)语句;
这里边的“语句”就是循环体语句,若其中只有一条语句,可以不用花括号;若多于一条,则必须用花括号将这些循环体语句括起来。
(1)先操作表达式1;
(2)操作表达式2,若其值为真(值为非O),则执行for
语句中的循环体语句一次,然后执行下面第三步,若为假
(值为O),则结束循环,转到第5步;
(3)操作表达式3;
(4)转回上面第2步骤继续执行;
(5)结束循环,执行for语句下面的语句。
7.C++虚析构函数调用问题
例1:
我们知道,用C++开发的时候,用来做基类的类的析构函数一般都是虚函数。可是,为什么要这样做呢?下面用一个小例子来说明:
有下面的两个类:
class ClxBase
{
public:
ClxBase() {};
virtual ~ClxBase() {};
virtual void DoSomething() { cout << "Do something in class ClxBase!" << endl; };
};
class ClxDerived : public ClxBase
{
public:
ClxDerived() {};
~ClxDerived() { cout << "Output from the destructor of class ClxDerived!" << endl; };
void DoSomething() { cout << "Do something in class ClxDerived!" << endl; };
};
代码
ClxBase *pTest = new ClxDerived;
pTest->DoSomething();
delete pTest;
的输出结果是:
Do something in class ClxDerived!
Output from the destructor of class ClxDerived!
这个很简单,非常好理解。
但是,如果把类ClxBase析构函数前的virtual去掉,那输出结果就是下面的样子了:
Do something in class ClxDerived!
也就是说,类ClxDerived的析构函数根本没有被调用!一般情况下类的析构函数里面都是释放内存资源,而析构函数不被调用的话就会造成内存泄漏。我想所有的C++程序员都知道这样的危险性。当然,如果在析构函数中做了其他工作的话,那你的所有努力也都是白费力气。
所以,文章开头的那个问题的答案就是--这样做是为了当用一个基类的指针删除一个派生类的对象时,派生类的析构函数会被调用。
当然,并不是要把所有类的析构函数都写成虚函数。因为当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里面来存放虚函数指针,这样就会增加类的存储空间。所以,只有当一个类被用来作为基类的时候,才把析构函数写成虚函数。
如果继承是多态的方式,则一定要将基类的析构函数设置为virtual形式的。
例2:
#include "stdafx.h"
#include <iostream>
using namespace std;
class Base
{
public:
virtual ~Base(){cout<<"~Base"<<endl;};
};
class Derived:public Base
{
public:
~Derived(){cout<<"~Derived"<<endl;}
};
int _tmain(int argc, _TCHAR* argv[])
{
//char *p="google";
//char pp[]="abcdefg";
//cout<<pp[1];
Base *p=new Derived;
//p->~Base();
delete p;
system("Pause");
return 0;
}
输出:~Derived ~Base,如果去掉virtual 则输出为:~Base;基类析构函数声明为虚函数时,就是动态绑定;否则就是静态绑定。
不管什么情况下,类的实例都会调用析构函数,没有自定义的,就用默认的,默认的析构函数可以清除类变量,如string之类(自带构造和析构函数的类)的变量,如要清除指向对象的指针,一定要自定义的析构函数。
8.拷贝构造函数(浅拷贝和深拷贝)
深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。
在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。
#include <iostream>
using namespace std;
class CA
{
public:
CA(int b,char* cstr)
{
a=b;
str=new char;
strcpy(str,cstr);
}
CA(const CA& C)
{
a=C.a;
str=new char[a]; //深拷贝
if(str!=0)
strcpy(str,C.str);
}
void Show()
{
cout<<str<<endl;
}
~CA()
{
delete str;
}
private:
int a;
char *str;
};
int main()
{
CA A(10,"Hello!");
CA B=A;
B.Show();
return 0;
}
深拷贝和浅拷贝的定义可以简单理解成:如果一个类拥有资源(堆,或者是其它系统资源),当这个类的对象发生复制过程的时候,这个过程就可以叫做深拷贝,反之对象存在资源,但复制过程并未复制资源的情况视为浅拷贝。
浅拷贝资源后在释放资源的时候会产生资源归属不清的情况导致程序运行出错。
Test(Test &c_t)是自定义的拷贝构造函数,拷贝构造函数的名称必须与类名称一致,函数的形式参数是本类型的一个引用变量,且必须是引用。
当用一个已经初始化过了的自定义类类型对象去初始化另一个新构造的对象的时候,拷贝构造函数就会被自动调用,如果你没有自定义拷贝构造函数的时候,系统将会提供给一个默认的拷贝构造函数来完成这个过程,上面代码的复制核心语句就是通过Test(Test &c_t)拷贝构造函数内的p1=c_t.p1;语句完成的。
9.深层揭秘 extern "C"
实现C++与C及其它语言的混合编程。
被extern "C"限定的函数或变量是extern类型的;extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。
(1)在C++中引用C语言中的函数和变量,在包含C语言头文件(假设为cExample.h)时,需进行下列处理:
extern "C"
{
#include "cExample.h"
}
而在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern "C"声明,在.c文件中包含了extern "C"时会出现编译语法错误。
笔者编写的C++引用C函数例子工程中包含的三个文件的源代码如下:
/* c语言头文件:cExample.h */
#ifndef C_EXAMPLE_H
#define C_EXAMPLE_H
extern int add(int x,int y);
#endif
/* c语言实现文件:cExample.c */
#include "cExample.h"
int add( int x, int y )
{
return x + y;
}
// c++实现文件,调用add:cppFile.cpp
extern "C"
{
#include "cExample.h"
}
int main(int argc, char* argv[])
{
add(2,3);
return 0;
}
如果C++调用一个C语言编写的.DLL时,当包括.DLL的头文件或声明接口函数时,应加extern "C" { }。
(2)在C中引用C++语言中的函数和变量时,C++的头文件需添加extern "C",但是在C语言中不能直接引用声明了extern "C"的该头文件,应该仅将C文件中将C++中定义的extern "C"函数声明为extern类型。
笔者编写的C引用C++函数例子工程中包含的三个文件的源代码如下:
//C++头文件 cppExample.h
#ifndef CPP_EXAMPLE_H
#define CPP_EXAMPLE_H
extern "C" int add( int x, int y );
#endif
//C++实现文件 cppExample.cpp
#include "cppExample.h"
int add( int x, int y )
{
return x + y;
}
/* C实现文件 cFile.c
/* 这样会编译出错:#include "cExample.h" */
extern int add( int x, int y );
int main( int argc, char* argv[] )
{
add( 2, 3 );
return 0;
}
10.子类对父类的同名函数的覆盖
如果一个类,存在和父类相同的函数,那么,这个类将会覆盖其父类的方法,除非你在调用的时候,强制转换为父类类型,否则试图对子类和父类做类似重载的调用是不能成功的。
关于函数重定义
class A
{
public:
void fun()
{
printf("A\n");
}
};
class B:public A
{
public:
void fun()
{
printf("B\n");
}
};
int main(int argc, char* argv[])
{
A a;
a.fun();
B b;
b.fun();
b.A::fun();
printf("\n");
return 0;
}
如果基类的函数是 virtual,在派生类里重定义,才会动态绑定,否则就是屏蔽了。这种程序本身就有很大的弊病,作为讨论可以,但真正使用的话,还是抛弃的好。子类尽量不要重新定义继承而来的非虚函数,这会导致“不变性凌驾特异性”的性质(effect
C++)混乱。如果要重写,把父类的相应函数定义为虚函数。子类会继承父类的所有成员,在子类中重定义父类的同名函数后,只是在用子类对象调用该函数是只会执行子类重定义后的函数,如果要调用父类的同名函数则要用::域运算符来调用!
父类虚函数,子类重新定义,但前面没有virtual关键字
#include
using namespace std;
class Parent{
public:
void virtual foo(){
cout << "A" << endl;
}
};
class Son:public Parent{
public:
//形成覆盖,子类重新定义父类的虚函数
void foo(){
cout << "foo from son" << endl;
}
};
int main(){
Parent *pa = new Parent();
pa->foo();
Son* pb = (Son*)pa;
pb->foo();
delete pa,pb;
pa = new Son();
pa->foo();
pb = (Son*)pa;
pb->foo();
return 0;
}
输出 :AABB
10 .[b]类成员函数的重载、覆盖和隐藏区别?
答案:
a.成员函数被重载的特征:
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。
b.覆盖是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。(子类的函数可以没有关键字virtual,也形成对父类函数的覆盖)
c.“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)
11.类成员变量和函数的地址
记住:函数名字本身就是一个指针
做下面的一个测试
view
plainprint?
#include<iostream>
using namespace std;
class A
{
public:
A(int);
void fun1();
virtual void fun2();
static void fun3();
int num1;
static int num2;
};
A::A(int i)
{
num1=i;
}
void A::fun1()
{
cout<<"I am in fun1"<<endl;
}
void A::fun2()
{
cout<<"I am in fun2"<<endl;
}
void A::fun3()
{
cout<<"I am in fun3"<<endl;
}
int A::num2=1;
void main()
{
A a(2);
//获取静态成员数据的地址
int *ptr_static=&A::num2;
cout<<"静态成员数据的地址"<<ptr_static<<endl;
ptr_static=&a.num2;
cout<<"a.num2静态成员数据的地址"<<ptr_static<<endl;
//获取静态函数的地址
void (*ptr_staticfun)=A::fun3;
cout<<"静态成员函数的地址"<<ptr_staticfun<<endl;
ptr_staticfun=a.fun3;
cout<<"a.fun3静态成员函数的地址"<<ptr_staticfun<<endl;
//获取普通成员函数的地址
typedef void (A::*ptr_commomfun)();
函数指针类型声明。
ptr_commomfun ptr=A::fun1;
//函数指针类型实例
cout<<"普通成员函数的地址"<<ptr<<endl; //如果直接输出ptr的话,输出来的是1,因此应该把ptr地址中的内容读出来
cout<<"普通成员函数的地址"<<*((long*)&ptr)<<endl;
ptr=a.fun1;
cout<<"a.fun1普通成员函数的地址"<<*((long*)&ptr)<<endl;
ptr_commomfun ptr_virtual=A::fun2; //获取虚函数的地址
cout<<"虚成员函数的地址"<<*((long*)&ptr_virtual)<<endl;
ptr_virtual=a.fun2;
cout<<"a.fun2虚成员函数的地址"<<*((long*)&ptr_virtual)<<endl;
int *p;
int A::*q;
q=&A::num1;
//指向数据成员的指针赋予的是一个目前还不存在的一个类成员的地址,而这个地址只有在使用实际类对象进行调用时才会真正的确定下来
//就是说在类还没有对象时候,成员变量时没有空间的。
cout<<"普通成员数据的地址"<<*((long *)&q)<<endl;
p=&a.num1;
cout<<"a.num2普通成员数据的地址"<<p<<endl;
}
注意在获取类成员函数的时候,如果直接把指针输出来,得到的是1,我想是因为编译器把&A::fun1当做bool变量
void (A::*ptr)();
ptr=A::fun1;或者ptr=&A::fun1都可以
另外需要注意的是
指向数据成员的指针赋予的是一个目前还不存在的一个类成员的地址,而这个地址只有在使用实际类对象进行调用时才会真正的确定下来
就是说在类还没有对象时候,成员变量时没有空间的
运行结果
12.函数指针、函数指针类型、函数类型
(1)函数指针
定义:函数指针是指指向函数的指针。像其他指针一样,函数指针也指向特定的类型。函数类型由其返回值以及形参表确定,而与函数名无关。e.g
void (*pf) ( char,int );
这个语句将pf声明指向函数的指针,它所指向的函数带有一个char类型,一个int类型,返回类型为void
我们可以这样理解:我们怎么定义普通的指针呢,如我们定义一个int型的指针,
int
*p;
是在变量声明前面加*,即p前面加上*号。而我们定义函数指针要在函数声明前加*, 函数声明为
void
pf( char,int );
函数声明前加*后变成
void
*pf(char,int);
我们把*pf用小括号括起来,变成
void (*pf) ( char,int ); 这就是函数指针的声明方法
测试代码如下:
#include"stdio.h"
void (*pf)(char, int);
void fun(char ,int); //声明一个函数,形参为一个char类型,一个int类型,返回类型为void
int main()
{
pf=fun; //给函数指针pf赋值为fun函数的地址(函数名代表函数的地址)
(*pf)('c',90); //调用pf指向的函数
}
void fun(char a,int b)
{
printf("the argument is %c and %d\n",a,b);
}
函数运行后的结果是
The argumeng is c and 90
(2)函数指针类型
函数指针类型相当地冗长。使用typedef为指针类型定义同义词,可将函数指针的使用大大简化typedef void (*FCN) (char,int);
记忆方法:在函数指针声明 void (*FCN)(char,int)前加上typedef关键字就是函数指针类型的声明。
该定义表示FCN是一种函数指针类型。该函数指针类型表示这样一类函数指针:
指向返回void类型并带有一个char类型,一个int类型的函数指针。
测试代码如下:
#include"stdio.h"
typedef void (*FCN)(char, int); //声明一个函数指针类型
void fun(char ,int); //声明一个函数,形参为一个char类型,一个int类型,返回类型为void
int main()
{
FCN pf;
pf=fun; //给函数指针pf赋值为fun函数的地址(函数名代表函数的地址)
(*pf)('c',90); //调用pf指向的函数
}
void fun(char a,int b)
{
printf("the argument is %c and %d\n",a,b);
}
要对绝对地址0x100000赋值,我们可以用
用typedef可以看得更直观些:
typedef void(*)()voidFuncPtr;
*((voidFuncPtr)0x100000)();
(3)函数类型
函数类型的定义:typedef void (*FCN)(char, int); //声明一个函数类型
该声明定义了一个函数类型,FCN表示这样一类函数,带有两个形参,一个是int ,一个是char,返回值是void型。一般用于函数声明和函数的形参。
一般我们在调用函数时,应该先声明要调用函数,如我们调用fun函数,则应在调用的前面声明void fun(char ,int);
如果我们定义了函数类型typedef void FCN(char , int);我们就可以这样声明函数原形,
FCN fun;
大大简化了函数原型的声明,函数类型用于形参的情况我们在下面讲解。
#include"stdio.h"
typedef void FCN(char , int);
int main()
{ FCN fun;
fun('c',90);
}
void fun(char a,int b)
{
printf("the argument is %c and %d\n",a,b);
}
13.C++三类继承方式
暂不考虑继承:
对类成员访问权限的控制,是通过设置成员的访问控制属性实现的。访问控制属性有以下三种:public,private和protected。
public成员:
任何一个来自类外部的访问都必须通过这种类型的成员来访问(“对象.公有成员”)。公有类型声明了类的外部接口。
private成员:
(若私有类型成员紧接着类名称,可省略关键字),私有类型的成员只允许本类的成员函数来访问,而类外部的任何访问都是非法的。这样完成了私有成员的隐蔽。
protected成员:
性质和私有类型的性质一致。即保护类型和私有类型的性质相似,其差别在于继承过程中对产生的新类影响不同。
C++中的继承方式有:
public、private、protected三种(它们直接影响到派生类的成员、及其对象对基类成员访问的规则)。
(1)public(公有继承):继承时保持基类中各成员属性不变,并且基类中private成员被隐藏。派生类的成员只能访问基类中的public/protected成员,而不能访问private成员;派生类的对象只能访问基类中的public成员。
(2)private(私有继承):继承时基类中各成员属性均变为private,并且基类中private成员被隐藏。派生类的成员也只能访问基类中的public/protected成员,而不能访问private成员;派生类的对象不能访问基类中的任何的成员。
(3)protected(保护性继承):继承时基类中各成员属性均变为protected,并且基类中private成员被隐藏。派生类的成员只能访问基类中的public/protected成员,而不能访问private成员;派生类的对象不能访问基类中的任何的成员。
14.析构函数
不管什么情况下,类的实例都会调用析构函数,没有自定义的,就用默认的,默认的析构函数可以清除类变量,如string之类(自带构造和析构函数的类)的变量,如要清除指向对象的指针,一定要自定义的析构函数。
15 继承与组合
若在逻辑上,A是B的一部分,则不许B从A继承,而是要用A和其他东西组合出B。
例如 眼,鼻子,耳朵是头的一部分,所以头应该有前三者组合而成,而不应该继承前三者。
如果头继承 眼睛、鼻子、耳朵等,则自动具有看、闻、听等功能(依据构父类和子类的构造函数的构造顺序)。
16 联合体union内存占用问题
structA
{
int o;
int j;
union
{
int i[10],j,k;
};
};
sizeof(A) //48
#pragma pack(1)
struct A
{
enum day{monring, moon, aftermoon};
};
sizeof(A) //1,结构体类型占用一个字节;
sizeof(A::day) //4,枚举成员占4个字节;
在C/C++程序的编写中,当多个基本数据类型或复合数据结构要占用同一片内存时,我们要使用联合体;当多种类型,多个对象,多个事物只取其一时(我们姑且通俗地称其为“n 选1”),我们也
可以使用联合体来发挥其长处。首先看一段代码:
union myun
{
struct { int x; int y; int z; }u;
int k;
}a;
int main()
{
a.u.x =4;
a.u.y =5;
a.u.z =6;
a.k = 0;
printf("%d %d %d\n",a.u.x,a.u.y,a.u.z);
return 0;
}
union类型是共享内存的,以size最大的结构作为自己的大小,这样的话,myun这个结构就包含u这个结构体,而大小也等于u这个结构体 的大小,在内存中的排列为声明的顺序x,y,z从低到高,然后赋值的时候,在内存中,就是x的位置放置4,y的位置放置5,z的位置放置6,现在对k赋 值,对k的赋值因为是union,要共享内存,所以从union的首地址开始放置,首地址开始的位置其实是x的位置,这样原来内存中x的位置就被k所赋的 值代替了,就变为0了,这个时候要进行打印,就直接看内存里就行了,x的位置也就是k的位置是0,而y,z的位置的值没有改变,所以应该是0,5,6
#i nclude <stdio.h>
union
{
int i;
charx[2];
}a;
voidmain()
{
a.x[0]= 10;
a.x[1]= 1;
printf("%d",a.i);
}
答案:266 (低位低地址,高位高地址,内存占用情况是Ox010A)
b)
main()
{
union{ /*定义一个联合*/
int i;
struct{ /*在联合中定义一个结构*/
char first;
char second;
}half;
}number;
number.i=0x4241; /*联合成员赋值*/
printf("%c%c\n",number.half.first, mumber.half.second);
number.half.first='a'; /*联合中结构成员赋值*/
number.half.second='b';
printf("%x\n", number.i);//按照十六进制输出
getch();
}
答案: AB (0x41对应'A',是低位;Ox42对应'B',是高位)//字符: 左 到 右 ——> 低 到 高
6261 (number.i和number.half共用一块地址空间)//数字: 右 到 左 ——> 高 到 低
备注:十六进制:A 41 B42
a 61 b 62
十进制: A 65 B 66
a 97 b 98
运算符sizeof可以计算出给定类型的大小,对于32位系统来说,sizeof(char) = 1; sizeof(int) = 4。基本数据类型的大小很好计算,我们来看一下如何计算构造数据类型的大小。
C语言中的构造数据类型有三种:数组、结构体和共用体。
数组是相同类型的元素的集合,只要会计算单个元素的大小,整个数组所占空间等于基础元素大小乘上元素的个数。
结构体中的成员可以是不同的数据类型,成员按照定义时的顺序依次存储在连续的内存空间。和数组不一样的是,结构体的大小不是所有成员大小简单的相加,需要考虑到系统在存储结构体变量时的地址对齐问题。看下面这样的一个结构体:
struct stu1
{
int i;
char c;
int j;
};
先介绍一个相关的概念——偏移量。偏移量指的是结构体变量中成员的地址和结构体变量地址的差。结构体大小等于最后一个成员的偏移量加上最后一个成员的大小。显然,结构体变量中第一个成员的地址就是结构体变量的首地址。因此,第一个成员i的偏移量为0。第二个成员c的偏移量是第一个成员的偏移量加上第一个成员的大小(0+4),其值为4;第三个成员j的偏移量是第二个成员的偏移量加上第二个成员的大小(4+1),其值为5。
实际上,由于存储变量时地址对齐的要求,编译器在编译程序时会遵循两条原则:一、结构体变量中成员的偏移量必须是成员大小的整数倍(0被认为是任何数的整数倍) 二、结构体大小必须是所有成员大小的整数倍。
对照第一条,上面的例子中前两个成员的偏移量都满足要求,但第三个成员的偏移量为5,并不是自身(int)大小的整数倍。编译器在处理时会在第二个成员后面补上3个空字节,使得第三个成员的偏移量变成8。
对照第二条,结构体大小等于最后一个成员的偏移量加上其大小,上面的例子中计算出来的大小为12,满足要求。
再看一个满足第一条,不满足第二条的情况
struct stu2
{
int k;
shortt;
};
成员k的偏移量为0;成员t的偏移量为4,都不需要调整。但计算出来的大小为6,显然不是成员k大小的整数倍。因此,编译器会在成员t后面补上2个字节,使得结构体的大小变成8从而满足第二个要求。由此可见,大家在定义结构体类型时需要考虑到字节对齐的情况,不同的顺序会影响到结构体的大小。对比下面两种定义顺序
struct stu3
{
char c1;
int i;
char c2;
}
struct stu4
{
char c1;
char c2;
int i;
}
虽然结构体stu3和stu4中成员都一样,但sizeof(struct stu3)的值为12而sizeof(struct stu4)的值为8。
如果结构体中的成员又是另外一种结构体类型时应该怎么计算呢?只需把其展开即可。但有一点需要注意,展开后的结构体的第一个成员的偏移量应当是被展开的结构体中最大的成员的整数倍。看下面的例子:
struct stu5
{
short i;
struct
{
char c;
int j;
} ss;
int k;
}
结构体stu5的成员ss.c的偏移量应该是4,而不是2。整个结构体大小应该是16。
如何给结构体变量分配空间由编译器决定,以上情况针对的是Linux下的GCC。其他平台的C编译器可能会有不同的处理。
17.关于运算符&& || 的面试题
bool Fun1(char* str)
{
printf("%s\n",str);
return false;
}
bool Fun2(char* str)
{
printf("%s\n",str);
return true;
}
int _tmain(int argc, _TCHAR* argv[])
{
bool res1,res2;
res1 = (Fun1("a")&& Fun2("b")) || (Fun1("c") || Fun2("d"));
res2 = (Fun1("a")&& Fun2("b")) &&(Fun1("c") || Fun2("d"));
return res1|| res2;
}
答案:打印出4行,分别是a、c、d、a。
在C/C++中,与、或运算是从左到右的顺序执行的。在计算rest1时,先计算Fun1(“a”)&& Func2(“b”)。首先Func1(“a”)打印出内容为a的一行。由于Fun1(“a”)返回的是false,无论Func2(“b”)的返回值是true还是false,Fun1(“a”)&& Func2(“b”)的结果都是false。由于Func2(“b”)的结果无关重要,因此Func2(“b”)会略去而不做计算。接下来计算Fun1(“c”)|| Func2(“d”),分别打印出内容c和d的两行。
在计算rest2时,首先Func1(“a”)打印出内容为a的一行。由于Func1(“a”)返回false,和前面一样的道理,Func2(“b”)会略去不做计算。由于Fun1(“a”)&& Func2(“b”)的结果是false,不管Fun1(“c”)&& Func2(“d”)的结果是什么,整个表达式得到的结果都是false,因此Fun1(“c”) || Func2(“d”)都将被忽略。
18.虚函数调用肯定是从虚函数表中调用,子类对父类形成覆盖
问题(25):运行下面的C++代码,打印的结果是什么?
class Base { public: voidprint() { doPrint();} private: virtual void doPrint() {cout << "Base::doPrint" << endl;} }; class Derived : public Base { private: virtual void doPrint() {cout << "Derived::doPrint" << endl;} }; int _tmain(int argc, _TCHAR* argv[]) { Base b; b.print(); Derived d; d.print(); return 0; }
13。关键字volatile有什么含意?并举出三个不同的例子?
答案:提示编译器对象的值可能在编译器未监测到的情况下改变。
14。int (*s[10])(int) 表示的是什么啊?
答案:int(*s[10])(int) 函数指针数组,每个指针指向一个int func(int param)的函数。
答案:输出两行,分别是Base::doPrint和Derived::doPrint。在print中调用doPrint时,doPrint()的写法和this->doPrint()是等价的,因此将根据实际的类型调用对应的doPrint。所以结果是分别调用的是Base::doPrint和Derived::doPrint2。如果感兴趣,可以查看一下汇编代码,就能看出来调用doPrint是从虚函数表中得到函数地址的。
19.C/C++ 语言中的表达式求值
问题一:经常可以在一些讨论组里看到下面的提问:“谁知道下面C语句给n赋什么值?”
m = 1; n = m+++m++;
问题二:问为什么在某个C++系统里,下面表达式打印出两个4,而不是4和5:
a = 4; cout << a++ << a;
要弄清这些,需要理解的一个问题是:如果程序里某处修改了一个变量(通过赋值、增量/减量操作等),什么时候从该变量能够取到新值?有人可能说,“这算什么问题!我修改了变量,再从这个变量取值,取到的当然是修改后的值!”其实事情并不这么简单。
C/C++ 语言是“基于表达式的语言”,所有计算(包括赋值)都在表达式里完成。“x = 1;”就是表达式“x = 1”后加表示语句结束的分号。要弄清程序的意义,首先要理解表达式的意义,也就是:1)表达式所确定的计算过程;2)它对环境(可以把环境看作当时可用的所有变量)的影响。如果一个表达式(或子表达式)只计算出值而不改变环境,我们就说它是引用透明的,这种表达式早算晚算对其他计算没有影响(不改变计算的环境。当然,它的值可能受到其他计算的影响)。如果一个表达式不仅算出一个值,还修改了环境,就说这个表达式有副作用(因为它多做了额外的事)。a++
就是有副作用的表达式。这些说法也适用于其他语言里的类似问题。
我们假定程序里有代码片段“...a[i]++ ... a[j] ...”,假定当时i与j的值恰好相等(a[i] 和a[j] 正好引用同一数组元素);假定a[i]++ 确实在a[j] 之前计算;再假定其间没有其他修改a[i] 的动作。在这些假定下,a[i]++ 对 a[i] 的修改能反映到 a[j] 的求值中吗?注意:由于 i 与 j 相等的问题无法静态判定,在目标代码里,这两个数组元素访问(对内存的访问)必然通过两段独立代码完成。现代计算机的计算都在寄存器里做,问题现在变成:在取 a[j] 值的代码执行之前,a[i]
更新的值是否已经被(从寄存器)保存到内存?如果了解语言在这方面的规定,这个问题的答案就清楚了。
程序语言通常都规定了执行中变量修改的最晚实现时刻(称为顺序点、序点或执行点)。程序执行中存在一系列顺序点(时刻),语言保证一旦执行到达一个顺序点,在此之前发生的所有修改(副作用)都必须实现(必须反应到随后对同一存储位置的访问中),在此之后的所有修改都还没有发生。在顺序点之间则没有任何保证。对C/C++ 语言这类允许表达式有副作用的语言,顺序点的概念特别重要。
现在上面问题的回答已经很清楚了:如果在a[i]++ 和a[j] 之间存在一个顺序点,那么就能保证a[j] 将取得修改之后的值;否则就不能保证。
C/C++语言定义(语言的参考手册)明确定义了顺序点的概念。顺序点位于:
1. 每个完整表达式结束时。完整表达式包括变量初始化表达式,表达式语句,return语句的表达式,以及条件、循环和switch语句的控制表达式(for头部有三个控制表达式);
2. 运算符 &&、||、?: 和逗号运算符的第一个运算对象计算之后;
3. 函数调用中对所有实际参数和函数名表达式(需要调用的函数也可能通过表达式描述)的求值完成之后(进入函数体之前)。
假设时刻ti和ti+1是前后相继的两个顺序点,到了ti+1,任何C/C++ 系统(VC、BC等都是C/C++系统)都必须实现ti之后发生的所有副作用。当然它们也可以不等到时刻ti+1,完全可以选择在时段 [t, ti+1] 之间的任何时刻实现在此期间出现的副作用,因为C/C++ 语言允许这些选择。
前面讨论中假定了a[i]++ 在a[i] 之前做。在一个程序片段里a[i]++ 究竟是否先做,还与它所在的表达式确定的计算过程有关。我们都熟悉C/C++ 语言有关优先级、结合性和括号的规定,而出现多个运算对象时的计算顺序却常常被人们忽略。看下面例子:
(a + b) * (c + d) fun(a++, b, a+5)
这里“*”的两个运算对象中哪个先算?fun及其三个参数按什么顺序计算?对第一个表达式,采用任何计算顺序都没关系,因为其中的子表达式都是引用透明的。第二个例子里的实参表达式出现了副作用,计算顺序就非常重要了。少数语言明确规定了运算对象的计算顺序(Java规定从左到右),C/C++ 则有意不予规定,既没有规定大多数二元运算的两个对象的计算顺序(除了&&、|| 和 ,),也没有规定函数参数和被调函数的计算顺序。在计算第二个表达式时,首先按照某种顺序算fun、a++、b和a+5,之后是顺序点,而后进入函数执行。
不少书籍在这些问题上有错(包括一些很流行的书)。例如说C/C++ 先算左边(或右边),或者说某个C/C++ 系统先计算某一边。这些说法都是错误的!一个C/C++ 系统可以永远先算左边或永远先算右边,也可以有时先算左边有时先算右边,或在同一表达式里有时先算左边有时先算右边。不同系统可能采用不同的顺序(因为都符合语言标准);同一系统的不同版本完全可以采用不同方式;同一版本在不同优化方式下,在不同位置都可能采用不同顺序。因为这些做法都符合语言规范。在这里还要注意顺序点的问题:即使某一边的表达式先算了,其副作用也可能没有反映到内存,因此对另一边的计算没有影响。
回到前面的例子:“谁知道下面C语句给n赋什么值?”
m = 1; n = m++ +m++;
正确回答是:不知道!语言没有规定它应该算出什么,结果完全依赖具体系统在具体上下文中的具体处理。其中牵涉到运算对象的求值顺序和变量修改的实现时刻问题。对于:
cout << a++ << a;
我们知道它是
(cout.operator <<(a++)).operator << (a);
的简写。先看外层函数调用,这里需要算出所用函数(由加下划线的一段得到),还需要计算a的值。语言没有规定哪个先算。如果真的先算函数,这一计算中出现了另一次函数调用,在被调函数体执行前有一个顺序点,那时a++的副作用就会实现。如果是先算参数,求出a的值4,而后计算函数时的副作用当然不会改变它(这种情况下输出两个4)。当然,这些只是假设,实际应该说的是:这种东西根本不该写,讨论其效果没有意义。
有人可能说,为什么人们设计 C/C++时不把顺序规定清楚,免去这些麻烦?C/C++ 语言的做法完全是有意而为,其目的就是允许编译器采用任何求值顺序,使编译器在优化中可以根据需要调整实现表达式求值的指令序列,以得到效率更高的代码。像Java那样严格规定表达式的求值顺序和效果,不仅限制了语言的实现方式,还要求更频繁的内存访问(以实现副作用),这些可能带来可观的效率损失。应该说,在这个问题上,C/C++和Java的选择都贯彻了它们各自的设计原则,各有所获(C/C++ 潜在的效率,Java更清晰的程序行为),当然也都有所失。还应该指出,大部分程序设计语言实际上都采用了类似C/C++的规定。
讨论了这么多,应该得到什么结论呢?C/C++ 语言的规定告诉我们,任何依赖于特定计算顺序、依赖于在顺序点之间实现修改效果的表达式,其结果都没有保证。程序设计中应该贯彻的规则是:如果在任何“完整表达式”(形成一段由顺序点结束的计算)里存在对同一“变量”的多个引用,那么表达式里就不应该出现对这一“变量”的副作用。否则就不能保证得到预期结果。注意:这里的问题不是在某个系统里试一试的问题,因为我们不可能试验所有可能的表达式组合形式以及所有可能的上下文。这里讨论的是语言,而不是某个实现。总而言之,绝不要写这种表达式,否则我们或早或晚会某种环境中遇到麻烦。
浅谈C++中的友元关系(转)
在封装中C++类数据成员大多情况是private属性;但是如果接口采用多参数实现肯定影响程序效率;然而这时候如果外界需要频繁访问这些私有成员,就不得不需要一个既安全又理想的“后门”——友元关系;C++中提供三种友元关系的实现方式,友元函数、友元成员函数、友元类。
友元函数:既将一个普通的函数在一个类中说明为一个friend属性;其定义(大多数会访问该类的成员)应在类后;
友元成员函数:既然是成员函数,那么肯定这个函数属于某个类,对了就是因为这个函数是另外一个类的成员函数,有时候因为我们想用一个类通过一个接口去访问另外一个类的信息,然而这个信息只能是被它授权的类才能访问;那么也需要用friend去实现;这个概念只是在声明的时候稍有变化;
友元类:友元类声明会将整个类说明成为另一个类的友元关系;和之前两种的区别是集体和个人的区别;友元类的所有成员函数都可以是另一个类的友元函数;
值得注意的是友元关系是单向的,有点像我们恋爱中出现的单相思 O(∩_∩)O,单向关系就是说如果A被说明成B的友元关系,那么只能说A是B的友元,并不代表B是A的友元;其次在多数情况下友元关系的函数都会访问它被说明中类的成员,这时候应该将函数定义在类的后面;
下面给一个简单的例程代码;
1 #include <iostream>
2
3 using namespace std;
4
5 class B;
6
7 class A{
8 private:
9 int x;
10 public:
11 A();
12 void display(B &);
13 };
14
15 class C;
16
17 class B{
18 private:
19 int y;
20 int z;
21 public:
22 B();
23 B(int, int);
24 friend void A::display(B &);//友元成员函数
25 friend void display(B &);//友元函数
26 friend class C;//友元类
27 };
28
29 class C{
30 private:
31 int sum;
32 void calc(B &);
33 public:
34 C();
35 void display(B &);
36 };
37
38 //必须在友元关系的类后进行定义
39 void display(B &v)//友元函数
40 {
41 cout << v.y << " " << v.z << endl;
42 }
43
44 A::A()
45 {
46 this->x = 0;
47 }
48
49 void A::display(B &v)//友元成员函数
50 {
51 this->x = v.y + v.z;
52 cout << this->x << endl;
53 }
54
55 B::B()
56 {
57 this->y = 0;
58 this->z = 0;
59 }
60
61 B::B(int y, int z)
62 {
63 this->y = y;
64 this->z = z;
65 }
66
67 C::C()
68 {
69 sum = 0;
70 }
71
72 void C::display(B &v)
73 {
74 this->calc(v);
75 cout << sum << " = " << v.y << " + " << v.z << endl;
76 }
77
78 void C::calc(B &v)
79 {
80 sum = v.y + v.z;
81 }
82
83 int main()
84 {
85 A a;
86 B b(2, 3);
87 display(b);
88 a.display(b);
89 C c;
90 c.display(b);
91
92 return 0;
93 }
94
相关文章推荐
- 实例解析C/C++疑难问题(二)
- C++普通函数指针与成员函数指针实例解析
- java socket解析和发送二进制报文工具(附java和C++转化问题)
- 解析:教你轻松的解决服务器的疑难问题
- 最大子数组问题-c++代码实现及运行实例结果
- C++接口定义,实现,继承接口类的子类,实例对象访问方法问题
- 雇佣问题(hireassistant)-c++代码实现及运行实例结果
- 雇佣问题随机排列数组(permuteBySorting)-c++代码实现及运行实例结果
- Visual Studio C++中的一些疑难问题(待更新)
- 在线雇佣问题(onlineMaximumHireAssistant)-c++代码实现及运行实例结果
- 最简单的0-1背包问题c++代码实例及运行结果
- 微软SQL Server 2014疑难解析问题摘录
- C++学习之五、理解C++疑难问题
- 问题二十:C++全局debug “ray tracing图形”实例
- 从LINQ实例解析LINQ的另类用法,解决多条件组合问题
- thrift实现一个C++实例遇到的问题
- c++连接mysql数据库出现无法解析外部符号的问题
- 最大子数组问题-c++代码实现及运行实例结果
- python web.py开发httpserver解决跨域问题实例解析
- C/C++刁钻问题各个击破之位运算及其应用实例(1)