实例解析C/C++疑难问题(二)
2011-08-29 19:54
253 查看
c++中的强制转换static_cast、dynamic_cast、reinterpret_cast的不同用法
序列化1、序列化是干什么的? 简单说就是为了保存在内存中的各种对象的状态,并且可以把保存的对象状态再读出来。虽然你可以用你自己的各种各样的方法来保存Object States,但是Java给你提供一种应该比你自己好的保存对象状态的机制,那就是序列化。 2、什么情况下需要序列化 a)当你想把的内存中的对象保存到一个文件中或者数据库中时候; b)当你想用套接字在网络上传送对象的时候; c)当你想通过RMI传输对象的时候; 3、当对一个对象实现序列化时,究竟发生了什么? 在没有序列化前,每个保存在堆(Heap)中的对象都有相应的状态(state),即实例变量(instance ariable)比如: Foo myFoo = new Foo(); myFoo .setWidth(37); myFoo.setHeight(70); 当通过下面的代码序列化之后,MyFoo对象中的width和Height实例变量的值(37,70)都被保存到foo.ser文件中,这样以后又可以把它 从文件中读出来,重新在堆中创建原来的对象。当然保存时候不仅仅是保存对象的实例变量的值,JVM还要保存一些小量信息,比如类的类型等以便恢复原来的对 象。 FileOutputStream fs = new FileOutputStream("foo.ser"); ObjectOutputStream os = new ObjectOutputStream(fs); os.writeObject(myFoo);
虚拟继承:
C++虚继承可以防止多重继承产生的二义性问题。
虚继承,就是在被继承的类前面加上virtual关键字,这时被继承的类称为虚基类,如下面代码中的base类。虚继承在多重继承的时可以防止二义性。
class base
class derived1 : virutal public base
class derived2 : virtual public base
class derived3 : public derived1, public derived2
以上的代码如果用到了base中的某个成员变量就不会产生二义性。和#progma once在头文件中的作用类似。请看下面的例子:
#include <iostream>
using namespace std;
class Parent
{
public:
int p; // p将会被所有的子类继承,也将是二义性的根源
inline Parent()
{
p = 10;
}
};
class Child1 : public Parent
{
public:
int c1;
inline Child1()
{
p = 12; // p在子类Child1中被赋值为12
c1 = 12;
}
};
class Child2 : public Parent
{
public:
int c2;
inline Child2()
{
p = 13; // p在子类Child2中被赋值为13
c2 = 13;
}
};
class GrandChild : public Child1, public Child2
{
public:
int grandchild;
// p显然也存在于GrandChild中,但是到底是12,还是13呢?这就产生了二义性
inline GrandChild()
{
grandchild = 14;
}
};
int main(void)
{
GrandChild* pGC = new GrandChild();
cout << pGC->p << endl;
return 0;
}
上面程序是不能通过编译的,编译器输出的错误信息如下:
…: error C2385: 'GrandChild::p' is ambiguous
…: warning C4385: could be the 'p' in base 'Parent' of base 'Child1'
of class 'GrandChild'
…: warning C4385: or the 'p' in base 'Parent' of base 'Child2' of
class 'GrandChild'
正如编译器告诉我们的那 样,GrandChild::p是模棱两可,它被Child1继承了即Child1中包含了一个Parent subobject,也被Child2继承了即Child2中也包含了一个Parent
suboject,然后GrandChild又同时继承了Child1和Child2,根据“derived class中要保持base class的完整原样性原则”,因此GrandChild包含了两个ParentObject。所以当pGC->p时,编译器根本无法确定是调用Child1::p还是Child2::p,由此边产生了模棱两可的情形。怎么解决这样的问题呢?答案就是用虚继承或者叫虚基类的方式。
在上面的示例程序中做如下改动:
class Child1 : public Parent -> class Child1 : virtual public
Parent
class Child2 : public Parent -> class Child2 : virtual public
Parent
GrandChild的定义维持不变:
class GrandChild : public Child1, public Child2
做了上述改动后,即增加了两个 virtual关键字,再编译就不会出现ambiguous之类的错误了。这是因为加上了virtual关键字后,就可以保证Parent suboject在GrandChild中只存在一份,从而消除了ambiguity。上面修改后的程序运行结果是13,这说明Child2类的那个p起 了作用,如果GrandChild的定义写成:
class GrandChild : public Child2,
public Child1
那么运行结果将是12。
上面的试验结果表面,在多重继承的时候,如果父类中有同名的成员变量(类似这篇文章中谈及的例子),为了防止二义性,一般要采用虚继承的方式,并且最右边的基类中的那个成员变量会出现在派生类对象中。
对于非托管资源,您在应用程序中使用完这些非托管资源之后,必须显示的释放他们,例如System.IO.StreamReader的一个文件对象,必须显示的调用对象的Close()方法关闭它,否则会占用系统的内存和资源,而且可能会出现意想不到的错误。
C#是完全面向对象的,C++也是面向对象的,但保留面向过程的语言特性,比如全局变量
C++内存字节对其的问题(转自:http://blog.csdn.net/zzffly9/article/details/1844421)
从union的sizeof问题看cpu的对界
考虑下面问题:(默认对齐方式)
union u { double a; int b; }; union u2 { char a[13]; int b; }; union u3 { char a[13]; char b; }; cout<<sizeof(u)<<endl; // 8 cout<<sizeof(u2)<<endl; // 16 cout<<sizeof(u3)<<endl; // 13 |
结论:复合数据类型,如union,struct,class的对齐方式为成员中对齐方式最大的成员的对齐方式。
顺便提一下CPU对界问题,32的C++采用8位对界来提高运行速度,所以编译器会尽量把数据放在它的对界上以提高内存命中率。对界是可以更改的,使用#pragma pack(x)宏可以改变编译器的对界方式,默认是8。C++固有类型的对界取编译器对界方式与自身大小中较小的一个。例如,指定编译器按2对界,int类型的大小是4,则int的对界为2和4中较小的2。在默认的对界方式下,因为几乎所有的数据类型都不大于默认的对界方式8(除了long double),所以所有的固有类型的对界方式可以认为就是类型自身的大小。更改一下上面的程序:
#pragma pack(2) union u2 { char a[13]; int b; }; union u3 { char a[13]; char b; }; #pragma pack(8) cout<<sizeof(u2)<<endl; // 14 cout<<sizeof(u3)<<endl; // 13 |
结论:C++固有类型的对界取编译器对界方式与自身大小中较小的一个。
9、struct的sizeof问题
因为对齐问题使结构体的sizeof变得比较复杂,看下面的例子:(默认对齐方式下)
struct s1 { char a; double b; int c; char d; }; struct s2 { char a; char b; int c; double d; }; cout<<sizeof(s1)<<endl; // 24 cout<<sizeof(s2)<<endl; // 16 |
对于s1,首先把a放到8的对界,假定是0,此时下一个空闲的地址是1,但是下一个元素d是double类型,要放到8的对界上,离1最接近的地址是8了,所以d被放在了8,此时下一个空闲地址变成了16,下一个元素c的对界是4,16可以满足,所以c放在了16,此时下一个空闲地址变成了20,下一个元素d需要对界1,也正好落在对界上,所以d放在了20,结构体在地址21处结束。由于s1的大小需要是8的倍数,所以21-23的空间被保留,s1的大小变成了24。
对于s2,首先把a放到8的对界,假定是0,此时下一个空闲地址是1,下一个元素的对界也是1,所以b摆放在1,下一个空闲地址变成了2;下一个元素c的对界是4,所以取离2最近的地址4摆放c,下一个空闲地址变成了8,下一个元素d的对界是8,所以d摆放在8,所有元素摆放完毕,结构体在15处结束,占用总空间为16,正好是8的倍数。
这里有个陷阱,对于结构体中的结构体成员,不要认为它的对齐方式就是他的大小,看下面的例子:
struct s1 { char a[8]; }; struct s2 { double d; }; struct s3 { s1 s; char a; }; struct s4 { s2 s; char a; }; cout<<sizeof(s1)<<endl; // 8 cout<<sizeof(s2)<<endl; // 8 cout<<sizeof(s3)<<endl; // 9 cout<<sizeof(s4)<<endl; // 16; |
所以,在自己定义结构体的时候,如果空间紧张的话,最好考虑对齐因素来排列结构体里的元素。
结论:struct 里面的元素是顺序存储的,每个元素占用的字节数根据对齐字节数N(struct 里占用字节最多的元素与CPU对齐字节数中较小的一个)进行调整.如果从左至右M个元素加起来的字节数大于N,则按从右至左舍去K个元素直至M-K个元素加起来的字节数小于等于N,如果等于N则不用字节填充,小于N则把M-K-1的元素填充直至=N.
静态变量针对类而不针对对象,无论这个类定义了多少对象,所有的对象共享唯一的一个静态变量。静态变量只能被静态函数调用而也正是因为静态成员是一个成员对应多个类而this是要具体到每个对象二者矛盾所以对应静态成员没有this指针
用静态成员函数访问静态成员。为了数据的安全性,不让外界随便访问静态成员,故将他做成私有的,提供静态成员函数来访问静态成员。静态成员函数并不接受对象的牵制,可以将它看作是某个命名空间的函数。静态成员函数被调用时,没有当前 对象的信息,所以静态成员函数不能访问数据成员。即没有this 指针,如果给静态成员函数的参数传入一个当前对象的引用,则可以访问普通数据成员。static int printnumber(Student& s){cout<<name<<endl;}
静态成员函数不可以同时声明为 virtual、const、volatile函数。举例如下:
class base{
virtual static void func1();//错误
static void func2() const;//错误
static void func3() volatile;//错误
};
友元函数:解决频繁调用的问题
成员函数以对象为自我中心,必须捆绑一个对象才能引用。
对于一个完整的程序,在内存中的分布情况如下图:
代码区
全局数据区
堆区
栈区
全局数据区的整个数据区在程序启动时被初始化为 0;
1.C++ 静态全局数据 与 全局数据 的差别:
(1)全局数据为程序数据,静态全局数据为文件数据,程序只有一个文件运行时,二者没有差别。
他们具有一次定义的原则。即多次生声明,一次定义。extern关键字。
2. 全局常量:
可以访问,但是不能被修改。不会扰乱模块的独立性,不允许在同一文件中反复定义,每个文件最多定义一次,但是可以在不同的程序文件中重复定义。
3.静态局部数据(也在全局数据区);
函数中定义,函数第一次被调用时就建立,生命周期与上述的一致。待到程序结束。在第一次被调用时初始化,以后调用不再初始化。
静态局部数据:函数内的,驻留在全局数据区,从生命周期来说,他与全局数据是一致的。
作用:可以确定函数是否被调用过。通过查看函数调用权值,计算函数被调用多少次。
找错
void test1()
{
char string[10];
char* str1="0123456789";
strcpy(string, str1);
}
答:表面上并且编译都不会错误。但如果string数组原意表示的是字符串的话,那这个赋值就没有达到意图。最好定义为char string[11],这样最后一个元素可以存储字符串结尾符'\0';
void test2()
{
char string[10], str1[10];
for(int I=0; I<10;I++)
{
str1[I] ='a';
}
strcpy(string, str1);
}
答:strcpy使用错误,strcpy只有遇到字符串末尾的'\0'才会结束,而str1并没有结尾标志,导致strcpy函数越界访问,不妨让str1[9]='\0',这样就正常了。
指针和引用的区别
引用是C++中的概念,初学者容易把引用和指针混淆一起。
一下程序中,n是m的一个引用(reference),m是被引用物(referent)。
int m;
int &n = m;
n相当于m的别名(绰号),对n的任何操作就是对m的操作。
所以n既不是m的拷贝,也不是指向m的指针,其实n就是m它自己。
引用的规则:
(1)引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。
(2)不能有NULL引用,引用必须与合法的存储单元关联(指针则可以是NULL)。
(3)一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)。
以下示例程序中,k被初始化为i的引用。
语句k = j并不能将k修改成为j的引用,只是把k的值改变成为6。
由于k是i的引用,所以i的值也变成了6。
int i = 5;
int j = 6;
int &k = i;
k = j; // k和i的值都变成了6;
引用的主要功能是传递函数的参数和返回值。
C++语言中,函数的参数和返回值的传递方式有三种:值传递、指针传递和引用传递。
以下是"值传递"的示例程序。
由于Func1函数体内的x是外部变量n的一份拷贝,改变x的值不会影响n, 所以n的值仍然是0。
void Func1(int x)
{
x = x + 10;
}
...
int n = 0;
Func1(n);
cout << "n = " << n << endl; // n = 0
以下是"指针传递"的示例程序。
由于Func2函数体内的x是指向外部变量n的指针,改变该指针的内容将导致n的值改变,所以n的值成为10。
void Func2(int*x)
{
(* x) = (* x) + 10;
}
...
int n = 0;
Func2(&n);
cout << "n = " << n << endl; // n = 10
以下是"引用传递"的示例程序。
由于Func3函数体内的x是外部变量n的引用,x和n是同一个东西,改变x等于改变n,所以n的值成为10。
void Func3(int&x)
{
x = x + 10;
}
...
int n = 0;
Func3(n);
cout << "n = " << n << endl; // n = 10
对比上述三个示例程序,会发现"引用传递"的性质象"指针传递",而书写方式象"值传递"。
实际上"引用"可以做的任何事情"指针"也都能够做,为什么还要"引用"这东西?
答案是"用适当的工具做恰如其分的工作"。
指针能够毫无约束地操作内存中的任何东西,尽管指针功能强大,但是非常危险。
如果的确只需要借用一下某个对象的"别名",那么就用"引用",而不要用"指针",以免发生意外。
1.指针
(1)NULL指针
NULL是C语言标准定义的一个值,这个值其实就是0,只不过为了使得看起来更加具有意义,才定义了这样的一个宏,中文的意思是空,表明不指向任何东西。你懂得。不过这里不讨论空和零的区别,呵呵。
在C语言中,NULL其实就是0,就像前面说的指针可以理解成特殊的int,它总是有值的,p=NULL,其实就是p的值等于0。对于不多数机器而言,0地址是不能直接访问的,设置为0,就表示该指针哪里都没指向。
当然,就机器内部而言,NULL指针的实际值可能与此不同,这种情况下,编译器将负责零值和内部值之间的翻译转换。
NULL指针的概念非常有用,它给了你一种方法,表示某个特定的指针目前并未指向任何东西。例如,一个用于在某个数组中查找某个特定值的函数可能返回一个指向查找到的数组元素的指针。如果没找到,则返回一个NULL指针。
在内存的动态分配上,NULL的意义非同凡响,我们使用它来避免内存被多次释放,造成经常性的段错误(segmentation fault)。一般,在free或者delete掉动态分配的内存后,都应该立即把指针置空,避免出现所以的悬挂指针,致使出现各种内存错误!例如:
int *p=(int*)malloc(sizeof(int));
*p=23;
free(p);
p=NULL;
free函数是不会也不可能把p置空的。像下面这样的代码就会出现内存段错误:
int *p=(int*)malloc(sizeof(int));
*p=23;
free(p);
free(p);
因为,第一次free操作之后,p指向的内存已经释放了,但是p的值还没有变化,free函数改不了这个值,再free一次的时候,p指向的内存区域已经被释放了,这个地址已经变成了非法地址,这个操作将导致段错误的发生(此时,p指向的区域刚好又被分配出去了,但是这种概率非常低,而且对这样一块内存区域进行操作是非常危险的!)
但是下面这段代码就不会出现这样的问题:
int *p=(int*)malloc(sizeof(int));
*p=23;
free(p);
p=NULL;
free(p);
因为p的值编程了NULL,free函数检测到p为NULL,会直接返回,而不会发生错误。
这里顺便告诉大家一个内存释放的小窍门,可以有效的避免因为忘记对指针进行置空而出现各种内存问题。这个方法就是自定义一个内存释放函数,但是传入的参数不知指针,而是指针的地址,在这个函数里面置空,如下:
#include<iostream>
#include<stdlib.h>
using namespace std;
void my_free(void *p){
void **tp=(void **)p;
if(NULL==*tp)
return ;
free(*tp);
*tp=NULL;
}
int main(int argc, char **argv){
int *p=new int;
*p=1;
cout<<p<<endl;
my_free(&p);
cout<<p<<endl;
free(p);
return 0;
}
结果:
my_free调用了之后,p的值就变成了0(NULL),调用多少次free都不会报错了!
另外一个方式也非常有效,那就是定义FREE宏,在宏里面对他进行置空。例如
#include<iostream>
#include<stdlib.h>
using namespacestd;
#define FREE(x) if(NULL==x) return; free(x); x=NULL
int main(intargc, char **argv){
int *p=new int;
*p=1;
cout<<p<<endl;
FREE(p);
cout<<p<<endl;
free(p);
return 0;
}
执行结果同上面一样,不会报段错误:
(关于内存的动态分配,这是个比较复杂的话题,有机会再专门开辟一章给各位讲述一下吧,写个帖子还是很花费时间和精力的,呵呵,写过的童鞋应该都很清楚,所以顺便插一句,转帖可以,请注明出处,毕竟,大家都是本着共享的精神来讨论问题的,写的好坏都没有向你所要什么,请尊重每个人的劳动成果。)
(2)void指针
虽然从字面上看,void的意思是空,但是void指针的意思,可不是空指针的意思,空指针指的是上面所说的NULL指针。
void指针实际上的意思是指向任意类型的指针。任意类型的指针都可以直接赋给void指针,而不需要进行强制转换。
例如:
Type a, *p=&a;(Type等于char, int, struct, int *…)
void *pv;
pv=p;
就像前面说的,void指针的好处,就在于,任意的指针都可以直接赋值给它,这在某些场合非常有用,因此有些操作对于任意指针都是相同的。void指针最常用于内存管理。最典型的,也是大家最熟知的,就是标准库的free函数。它的原型如下:
voidfree(void*ptr);
free函数的参数可以是任意指针,没有谁见过free参数里面的指针需要强壮为void*的吧?
malloc,calloc,realloc这些函数的返回值也是void指针,因为内存分配,实际上只需要知道分配的大小,然后返回新分配内存的地址就可以了,指针的值就是地址,返回的不管是何种指针,其实结果都是一样的,因为所有的指针长度其实都是32位的(32位机器),它的值就是内存的地址,指针类型只是给编译器看的,目的是让编译器在编译的时候能够正确的设置指针的值(参见指针运算章节)。如果malloc函数设置成下面这样的原型,完全没有问题。
char*malloc(size_tsz);
实际上设置成
Type*malloc(size_tsz);
也是完全正确的,使用void指针的原因,实际上就像前面说的,void指针意思是任意指针,这样设计更加严谨一些,也更符合我们的直观理解。如果对前面我说的指针概念理解的童鞋,肯定明白这一点。
(3)未初始化和非法指针
经常有面试,会考这样的代码校错:
int *a;
…
*a=12;
这段代码,在*a=12这里出了问题。这里的问题就在于,a究竟指向哪里?我们声明了这个变量,但是从未对它进行初始化,一般而言,没有初始化,a的值是任意的,随机的。如果a是全局变量或者static类型,它会被初始化为0(前面说过,其实指针可以理解成值是内存地址的int),但是不管哪种方式,这种方式的赋值都是非常危险的,如果你有着中体彩头号彩票的运气,a的值刚好等于某个变量或者分配内存的地址,那么这里的运行不会报错,但这时候的运气却不是什么好运,相反,是非常倒霉!因为这是对一块不属于你的内存进行操作,这实在是太危险了!如果a的初始值是个非法地址,这个赋值语句在执行的时候将会报错,从而终止程序吗,这个错误同样是段错误(segmentation
fault),如果是这样,你是幸运的,因为你发现了它,这样就可以修正它。
关于这种问题,编译器可能会,也可能不会对它进行检测。GNU的编译器是会进行检测的,会对未初始化的指针或变量输出警告信息。
(4)多级指针(也叫指针的指针)
其实如果对前面的指针概念完全理解了,这里都可以略过。指针的指针,无非就是指针指向的数据类型是指针罢了。
Type *p;
其中Type类型是指针,比如可以是int*,也可以是int **,这样p对应的就是二级指针和三级指针。一级指针的值存放的是数据的地址,二级指针的值存放的一级指针的地址,三级指针的值存放的是二级指针的地址,依此类推…
(5)函数指针
跟普通的变量一样,每一个函数都是有其地址的,我们通过跳转到这个地址执行代码来进行函数调用,只是,跟取普通数据不同的在于,函数有参数和返回值,在进行函数调用的时候,首先需要将参数压入栈中,调用完成后又需要将参数压入栈中。既然函数也是通过地址来进行访问的,那它也可以使用指针来指向,事实上,每一个函数名都是一个指针,不过它是指针常量和常量指针,它的值是不能改的,指向的值也不能改。
(关于常量指针和指针常量什么的,有时间在专门开辟一章来说明const这个东东吧,也是很有讲头的一个东东。。。)
函数指针一般用来干什么呢?函数指针最常用的场合就是回调函数。回调函数,顾名思义,就是某个函数会在适当的时候被别人调用。当期望你调用的函数能够使用你的某些方式去操作的时候,回调函数就很有用,比如,你期望某个排序函数在比较的时候,能够使用你定义的比较方法去比较。
如果想知道回调函数在实际中有什么作用,先假设有这样一种情况,我们要编写一个库,它提供了某些排序算法的实现,如冒泡排序、快速排序、shell排序、shake排序等等,但为使库更加通用,不想在函数中嵌入排序逻辑,而让使用者来实现相应的逻辑;或者,想让库可用于多种数据类型(int、float、string),此时,该怎么办呢?可以使用函数指针,并进行回调。留个接口,实现更通用的算法排序,各种数据类型的排序。
有过较深入的C编程经验的人应该都接触过。C的标准库中就有使用,例如在strlib.h头文件的qsort函数,它的原型为:
voidqsort(void*__base, size_t __nmemb, size_t __size, int(*_compar)(const void *,const void*));
其中int(*_compar)(const void*, const void *)就是回调函数,这个函数用于qsort函数用于数据的比较。下面,我会举一个例子,来描述qsort函数的工作原理。
一般,我们使用下面这样的方式来定义函数指针:
typedefint(*compare)(const void *x, const void *y);
这个时候,compare就是参数为const void *,const void *类型,返回值是int类型的函数。例如:
typedef int (*compare)(const void *x, const void *y);
int my_compare(const void *x, const void *y){
const int *a=(int *)x;
const int *b=(int *)y;
if(*a>*b)
return 1;
if(*a==*b)
return 0;
return -1;
}
void my_sort(void *data, int length, int size, compare){
char *d1,*d2;
//do something
if(compare(d1,d2)<0){
//do something
}else if(compare(d1,d2)==0){
//do something
}
else{
//do something
}
//do something
}
int main(int argc, char **argv){
int arr={2,4,3,656,23};
my_sort(arr, 5, sizeof(int), my_compare);
//do something
return 0;
}
用typedef来定义的好处,就是可以使用一个简短的名称来表示一种类型,而不需要总是使用很长的代码来,这样不仅使得代码更加简洁易读,更是避免了代码敲写容易出错的问题。强烈推荐各位在定义结构体,指针(尤其是函数指针)等比较复杂的结构时,使用typedef来定义。
2 关于双层循环:
(1)在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少CPU跨切循环层的次数。
(2)数组寻址时间,如果是大段连续空间,CPU的预读机制可以大大提高调用速度。
for (row=0; row <100; row++)
{
for ( col=0; col <5; col++ )
{
sum = sum + a[row][col];
}
}
for (col=0; col <5; col++ )
{
for (row=0; row <100; row++)
{
sum = sum + a[row][col];
}
}
如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体的外面。示例4-4(c)的程序比示例4-4(d)多执行了N-1次逻辑判断。并且由于前者老要进行逻辑判断,打断了循环“流水线”作业,使得编译器不能对循环进行优化处理,降低了效率。如果N非常大,最好采用示例4-4(d)的写法,可以提高效率。如果N非常小,两者效率差别并不明显,采用示例4-4(c)的写法比较好,因为程序更加简洁。
for (i=0; i <N; i++)
{
if (condition)
DoSomething();
else
DoOtherthing();
}
if (condition)
{
for (i=0; i <N; i++)
DoSomething();
}
else
{
for (i=0; i <N; i++)
DoOtherthing();
}
C和C++语言学习总结(资料来自<高质量C++/C 编程指南> 林锐博士 2001 年7 月24)知识结构:
1、if,for,switch,goto
2、#define,const
3、文件拷贝的代码,动态生成内存,复合表达式,strcpy,memcpy,sizeof
4、函数参数传递,内存分配方式,内存错误表现,malloc与new区别
5、类重载、隐藏与覆盖区别,extern问题,函数参数的缺省值问题,宏代码与内联函数区别
6、构造和析构的次序,String函数定义具体实现:
1、if,for,switch,goto
if:
bool int float pointer char 变量的使用方法
bool bParam;
int iParam;
float fParam;
int* pParam;
char cParam;
if(bParam) ,if(!bParam);
if(iParam == 0 ),if(iParam != 0 );
if(fParam>= -0.00001 && fParam <= 0.00001);
if(pParam == NULL),if(pParam != NULL);
if(cParam == '\0'),if(cParam != '\0');
if/else/return 的使用方法
if(condition) 可以等价为 return(condition?x:y);
{
return x;
}
else
{
return y;
}
for:
执行效率问题:
int row,col,sum;
int a[100][5];
for(row=0;row<100;row++) 效率低于 for(col=0;col<5;col++)
{ {
for(col=0;col<5;col++) for(row=0;row<100;row++)
{ {
sum = sum+a[row][col]; sum = sum+a[row][col];
} }
} }
int i;
for(i=0;i<N;i++) 效率低于if(condition)
{ {
if(condition) for(i=0;i<N;i++)
DoSomething(); DoSomething();
else }
DoOtherthing(); else
} {
for(i=0;i<N;i++)
DoOtherthing();
}
for (int x=0;x<=N-1;x++) 直观性差于 for(int x=0;x<N;x++)
switch:
switch(variable)
{
case value1: ...
break;
case value2: ...
break;
default: ...
break;
}
switch(c)中的c的数据类型可以是int,char,long,unsignedint,bool.
variable必须是整数或者强制为整数,由于char实际上是ASCII码,所以也可以.
c不可以是double,float,char*.
goto:
goto主要用于
{...
{...
{....
goto error;
}
}
}
error:
...
3 #define,const
#define和const区别
(1)#defineC语言
const C语言 C++语言
const常量有数据类型,编译器会进行类型安全检查,而#define没有数据类型,
const的常量可以进行调试,但宏常量不能进行调试.
(2)const的使用方法
在全局定义 constfloat PI=3.1415926
在类中定义
class A
{...
A(int size);
const int SIZE;
};
A::A(int size):SIZE(size)
{
...
}
对参数和函数的定义(const只能修饰输入参数,不能修饰输出参数)
const int x=1; 表示x的值是1,在程序中不能改变;
const int* x; 表示x代表的地址所指向的内容是不能改变得;
int const* x; 与constint* x;的表示的意思一样;
int * const x; 表示x代表的地址是不能改变的;
当是输入参数时,不需要是void Func(constint i),void Func(const int& i),可以是void Func(int i)
因为输入参数采用"值传递"(const inti),由于函数将自动产生临时变量用于复制该参数,该输入参数本来就无需保护,所以不要加const修饰;
不用constint& i的原因在于内部数据类型的参数不存在构造、析构的过程,而复制也非常快,"值传递"和"引用传递"的效率几乎相当.
当是输入参数时,不需要是void Func(const Aa),void Func(A a),可以是voidFunc(A& a)或voidFunc(const A& a)
不用const Aa,A a的原因是函数的效率比较低,因为函数体内将产生A类型的临时对象用于复制参数a,而临时对象的构造、复制和析构过程都需要消耗时间
最好用constA&a的原因是A&a中的a可以被改变,A&a和constA&a的好处在于都不会产生临时对象,效率高;
const A Func(const A&a )const的好处
第一个const表示返回的是个内部产生的对象,它不能被修改
const A Func(...)
{...}
const A a=Func(...);//不能是Aa=Func(...);
第二个const表示输入的参数是引用传递,函数内部不会产生临时对象,而且这个对象不能被内部修改
第三个const表示此函数内部的所涉及的数据成员不能修改
class Stack
{
int m_num;
int GetCount(void) const;
int Pop(void);
}
int Stack::GetCount(void) const
{
m_num++;//编译错误,企图修改数据成员m_num;
Pop();//编译错误,企图调用非const函数
}
4 文件拷贝的代码
#include <stdio.h>
int main(int argc, char* argv[])
{
printf("Hello World!\n");
FILE* in;
FILE* out;
in=fopen("d:\\1.txt","rb");
out=fopen("d:\\2.txt","wb");
char ch=fgetc(in);
while(!feof(in))
{
fputc(ch,out);
ch=fgetc(in);
}
fclose(in);
fclose(out);
return 0;
}
动态生成内存的代码
------------------------------------------
正确代码:
void GetMemory(char **p, int num) { *p = (char *)malloc(sizeof(char) * num);
}
malloc函数返回一个指针,这个指针就是p的内容,指向指针的指针定义方法 ,例如int **p; 定义了一个指针变量p,他指向另一个指针变量(该指针变量又指向一个整型变量),是一个2级指针,由于指针运算符*是按自右向左顺序结合,因此上述定义相当于:int *(*p); (*p)是指针变量形式,它外面的*表示p指向的又是一个指针变量,int表示后一个指针变量指向的是指针变量
怎样使一个指针变量指向另一个指针变量呢?
int **p1;
int *p2;
int i=3;
p2=&i; (使p2指向i)
p1=&p2; (使p1指向p2)
char* GetMemory2(int num)
{
char* p = (char *)malloc(sizeof(char) * num);
return p;
}
------------------------------------------
错误的代码:
void GetMemory3(char *p, int num)
{
p = (char *)malloc(sizeof(char) * num);
}
GetMemory3(str,100); // str 仍然为NULL,因为这个函数里面str只能做赋值元素,不能//写等号左边;因为此时的里面的str实际是一个副本而已。函数参数传递是值传递,头指针作为参数时,实际上是生成了一个指针的副本,函数中是对副本进行操作,没有改变原指针的值,所以指针仍保持进入函数前的值。上述函数只给P的副本赋值了,而没给P赋值。不过P和P的副本的内容是一样的,指向一样的内容。
------------------------------------------
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100); // 注意参数是&str,而不是str
strcpy(str, "hello");
cout<< str << endl;
free(str);
str=NULL;
str=GetMemory2(100);
strcpy(str, "hello");
cout<< str << endl;
free(str);
str=NULL;
strcpy(str, "hello"); // 运行错误
cout<< str << endl;//运行错误
free(str);//运行错误
}
strcpy代码
char* strcpy(char* strDest,const char* strSrc)
{
if(strDest==NULL||strSrc==NULL) return NULL;
char* pStr=strDest;
while((*strDest++=*strSrc++)!='\0)
NULL;
return pStr;
}
malloc与free是C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。(因为库函数功能固定,外界无法对其功能进行改变)
因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。
内部数据类型是编译器本来就认识的,不需要用户自己定义 非内部数据类型不是编译器本来就认识的,需要用户自己定义才能让编译器识别 运算符使用是否正确,编译器在编译扫描分析时就可以判定 库函数是已编译的代码,编译器不会编译检查,由链接器将库同用户写的代码合成exe文件 对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。
由于malloc/free是库函数,不是运算符,他们不能执行构造函数和析构函数,只是负责分配内存。 1、malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。 2、对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free.
3、因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete.注意new/delete不是库函数。 C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存 new 是个操作符,和什么“+”,“-”,“=”…有一样的地位。malloc是个分配内存的函数,供你调用的。 new是保留字,不需要头文件支持。malloc需要头文件库函数支持。 new 建立的是一个对象,malloc分配的是一块内存。 new建立的对象你可以把它当成一个普通的对象,用成员函数访问,不要直接访问它的地址空间。
malloc分配的是一块内存区域,就用指针访问好了,而且还可以在里面移动指针。 还有个问题,为什么库函数不在编译器控制权限内,而运算符在。
(理解了库函数和运算符以及内部数据类型的定义之后,这块彻底明白了)。
char* GetString1()
{
char p[] = "ppp";
return p;
}
char* GetString2()
{
char *p = "Hello World";
return p;
}
void _tmain(int argc, _TCHAR* argv[])
{
printf("GetString1 returns:%s. \n", GetString1());
printf("GetString2 returns:%s. \n", GetString2());
float x=0;
scanf("%f",&x);
}
//当运行到GetString1时,p是一个数组,会开辟一块内存,并拷贝"HelloWorld"初始化该数组。
//接着返回数组的首地址并退出该函数。由于p是GetString1内的一个局部变量,当运行到这个函数外面的时候,这个数组的内存会被释放掉。
//因此在_tmain函数里再去访问这个数组的内容时,结果是随机的。
//当运行到GetString2时,p是一个指针,它指向的是字符串常量区的一个常量字符串。
//该常量字符串是一个全局的,并不会因为退出函数GetString2而被释放掉。
//因此在_tmain中仍然根据GetString2返回的地址得到字符串"Hello World"。
复合表达式
d = (a = b + c) + r ;
该表达式既求a 值又求d 值.应该拆分为两个独立的语句:
a = b + c;
d = a + r;
if (a < b < c) // a < b < c 是数学表达式而不是程序表达式
并不表示
if ((a<b) && (b<c))
而是成了令人费解的
if ( (a<b)<c )//条件真 1假0;
memcpy代码
void* memcpy(char* strDest,const char* strSrc,size_t size)
{
if(strDest==NULL||strSrc==NULL) return NULL;
if(size<=0) return NULL;
char* pStr=strDest;
while(size-->0)
*strDest++=*strSrc++;
return pStr;
}
sizeof:
i.在32位操作系统中,基本数据类型
类型 字节长度
char 1
short 2
short int 2
signed short 2
unsigned short 2
int 4
long int 4
signed int 4
unsigned int(unsigned) 4
long 4
unsigned long 4
float 4
double 8
void* 4 (所有指针类型长度都一样)(char*,int*,float*,double*)
enum 4
ii.在32位操作系统中,定义或函数中的大小
char a[]="hello";
char b[100];
char *p=a;
类型 字节长度
sizeof(a) 6
sizeof(b) 100
sizeof(p) 4
1.八进制整常数八进制整常数必须以0开头,即以0作为八进制数的前缀。数码取值为0~7。八进制数通常是无符号数。
以下各数是合法的八进制数:
015(十进制为13) 0101(十进制为65) 0177777(十进制为65535)
以下各数不是合法的八进制数:
256(无前缀0) 03A2(包含了非八进制数码) -0127(出现了负号)
2.十六进制整常数。十六进制整常数的前缀为0X或0x。其数码取值为0~9,A~F或a~f。 以下各数是合法的十六进制整常数:
0X2A(十进制为42) 0XA0 (十进制为160) 0XFFFF (十进制为65535)
以下各数不是合法的十六进制整常数:
5A (无前缀0X) 0X3H (含有非十六进制数码)
3.十进制整常数。十进制整常数没有前缀。其数码为0~9。
以下各数是合法的十进制整常数:
237 -568 65535 1627
以下各数不是合法的十进制整常数:
023 (不能有前导0) 23D (含有非十进制数码)
在程序中是根据前缀来区分各种进制数的。因此在书写常数时不要把前缀弄错造成结果不正确。
4.整型常数的后缀在16位字长的机器上,基本整型的长度也为 16位,因此表示的数的范围也是有限定的。
十进制无符号整常数的范围为0~65535,有符号数为-32768~+32767。八进制无符号数的表示范围为0~0177777。十六进制无符号数的表示范围为0X0~0XFFFF或0x0~0xFFFF。
如果使用的数超过了上述范围,就必须用长整型数来表示。长整型数是用后缀“L”或“l”来表示的。例如:
十进制长整常数 158L (十进制为158) 358000L (十进制为-358000)
八进制长整常数 012L (十进制为10) 077L (十进制为63) 0200000L (十进制为65536)
十六进制长整常数 0X15L (十进制为21) 0XA5L (十进制为165) 0X10000L (十进制为65536)
长整数158L和基本整常数158在数值上并无区别。但对158L,因为是长整型量,C编译系统将为它分配4个字节存储空间。而对158,因为是基本整型,只分配2 个字节的存储空间。因此在运算和输出格式上要予以注意,避免出错。
无符号数也可用后缀表示,整型常数的无符号数的后缀为“U”或“u”。例如: 358u,0x38Au,235Lu 均为无符号数。前缀,后缀可同时使用以表示各种类型的数。如0XA5Lu表示十六进制无符号长整数A5,其十进制为165。
1.1.2 整型变量
整型变量可分为以下几类:
1.基本型
类型说明符为int,在内存中占2个字节,其取值为基本整常数。
2.短整量
类型说明符为short int或short。所占字节和取值范围均与基本型相同。
3.长整型
类型说明符为long int或long,在内存中占4个字节,其取值为长整常数。
4.无符号型
类型说明符为unsigned。
无符号型又可与上述三种类型匹配而构成:
(1)无符号基本型 类型说明符为unsigned int或unsigned。
(2)无符号短整型 类型说明符为unsigned short
(3)无符号长整型 类型说明符为unsigned long
Notice:unsigned short 与 unsigned long的区别?
各种无符号类型量所占的内存空间字节数与相应的有符号类型量相同。但由于省去了符号位,故不能表示负数。
void Func(char a[100])
{
sizeof(a); //4
}
voidFunc(chara[100])
{
cout<<sizeof(a); //4
}
void _tmain(int argc, _TCHAR* argv[])
{
chara[]="hello";
charb[100];
char*p=a;
int m=0;
cout<<sizeof(a)<<endl;//6
cout<<sizeof(b)<<endl;//100在sizeof(str)中,str表示的含义是str[],因此返回整个数组的大小
cout<<sizeof(p)<<endl;//所有类型指针输出为4
cout<<sizeof(m)<<endl;//32位机中整数int输出为4
Func(b);//4数组名作函数参数不是“值传递”,而是把实际数组的起始地址传递给形参数,即两个数组占用同一段内存。这种方式叫“地址传递”。因
此,形参数组中元素的值发生变化会使实参数组元素的值同时发生变化。
cin>>m;
}
#pragma pack(1)
struct A
{
int i;
char j;
};
sizeof(A) //5
#pragma pack(1)
struct A
{
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;
char x[2];
}a;
void main()
{
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共用一块地址空间)
运算符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编译器可能会有不同的处理。
1.枚举类型的定义:
枚举类型也是一种自定义的复合类型。不过,枚举类型中的成员都是常量。
如enum color
{
red,
green,
blue,
white,
black
};
枚举类型中的成员默认值为从0开始,依次序递增。此时red==0,green为1,blue为2,white为3,black为4.
也可以改变起默认值。
如
enum color
{
red=1,
green=3,
blue=5,
white,
black
};
没有初始化的枚举类型成员的值将在它前面的成员基础上递增。
所以,white的值为6,而black的值为7.
enum string
{
x1,
x2,
x3=10,
x4,
x5,
}x;
问x;
答案:取值在0。1。10。11。12中的一个
2.定义枚举变量:
color a1,a2;
3.给枚举变量赋值:
a1=red;
a2=blue;
cout<<a1<<a2;//输出结果是15
虽然枚举常量的值整数,但是不能直接将整数值赋给枚举变量。
如
a1=1;//这是不对的。因为类型不匹配。一个是整型,一个是枚举类型。
a1=(color)1;//正确
枚举变量的size是一个整数的大小。
一、函数参数传递:
C++语言中,函数的参数和返回值的传递方式有三种:值传递、指针传递和引用传递.
(1)值传递
"值传递"的示例程序.由于Func1 函数体内的x 是外部变量n 的一份拷贝,
改变x 的值不会影响n, 所以n 的值仍然是0.
void Func1(int x)
{
x = x + 10;
}
…
int n = 0;
Func1(n);
cout << "n = " << n << endl; // n = 0
(2)指针传递
"指针传递"的示例程序.由于Func2 函数体内的x 是指向外部变量n 的指
针,改变该指针的内容将导致n 的值改变,所以n 的值成为10.
void Func2(int *x)
{
(* x) = (* x) + 10;
}
…
int n = 0;
Func2(&n);
cout << "n = " << n << endl; // n = 10
(3)引用传递
"引用传递"的示例程序.由于Func3 函数体内的x 是外部变量n 的引用,x
和n 是同一个东西,改变x 等于改变n,所以n 的值成为10.
void Func3(int &x)
{
x = x + 10;
}
…
int n = 0;
Func3(n);
cout << "n = " << n << endl; // n = 10
二、内存分配方式
分配方式 变量类型 分配特点
静态存储区域分配 全局变量,static 变量 内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在.
栈分配 函数内局部变量 栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限.
堆分配(亦称动态内存分配) new ,malloc分配 用malloc 或new 申请任意多少的内存,程序员自己负责在何时用free 或delete 释放内存。
内存错误
内存分配未成功,却使用了它.
内存分配虽然成功,但是尚未初始化就引用它.
内存分配成功并且已经初始化,但操作越过了内存的边界. 例如在使用数组时经常发生下标"多1"或者"少1"的操作.特别是在for 循环语句中,循环次数很容易搞错,导致数组操作越界.
忘记了释放内存,造成内存泄露.
放了内存却继续使用它.
函数的return 语句写错了,注意不要返回指向"栈内存"的"指针"或者"引用",因为该内存在函数体结束时被自动销毁.
程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面.
使用free 或delete 释放了内存后,没有将指针设置为NULL.导致产生"野指针".
5、类重载、隐藏与覆盖区别
成员函数被重载的特征:
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无.
覆盖是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字.
#include<iostream.h>
class Base
{
public:
void f(int x) { cout << "Base::f(int) " << x<< endl; }
void f(float x) { cout << "Base::f(float) " << x<< endl; }
virtual void g(void) { cout << "Base::g(void)" <<endl;}
void h(float x) { cout << "Base::h(float) " << x<< endl;}
void k(float x) { cout << "Base::k(float) " << x<< endl;}
};
class Derived : public Base
{
public:
virtual void g(void) { cout << "Derived::g(void)"<< endl;}
void h(int x) { cout << "Derived::h(int) " << x<< endl; }
void k(float x) { cout << "Derived::k(float) " <<x << endl;}
};
void main(void)
{
Derived d;
Base*pb = &d;
Derived *pd = &d;
pb->f(42); // Base::f(int) 42 //重载
pb->f(3.14f); //Base::f(float) 3.14 //重载
pb->g(); // Derived::g(void) //覆盖
pd->g(); // Derived::g(void) //覆盖
pb->h(3.14f) // Base::h(float) 3.14 //隐藏
pd->h(3.14f) //Derived::h(int) 3 //隐藏
pb->k(3.14f) //Base::k(float) 3.14 //隐藏
pd->k(3.14f) //Derived::k(float) 3.14 //隐藏
}
extern问题
如果C++程序要调用已经被编译后的C 函数,该怎么办?
假设某个C 函数的声明如下:
void foo(int x, int y);
该函数被C 编译器编译后在库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字用来支持函数重载和类型安全连接.由于编译后的名字不同,C++程序不能直接调用C 函数.C++提供了一个C 连接交换指定符号extern"C"来解决这个问题.例如:
//表示该函数在其他地方有定义。
extern "C"
{
void foo(int x, int y);
… // 其它函数
}
或者写成
extern "C"
{
#include "myheader.h"
… // 其它C 头文件
}
这就告诉C++编译译器,函数foo 是个C 连接,应该到库中找名字_foo 而不是找_foo_int_int.C++编译器开发商已经对C 标准库的头文件作了extern"C"处理,所以我们可以用#include 直接引用这些头文件.
函数参数的缺省值问题
正确方法:
void Foo(int x=0, int y=0); // 正确,缺省值出现在函数的声明中
void Foo(int x,int y)
{
...
}
错误方法:
void Foo(int x=0, int y=0) // 错误,缺省值出现在函数的定义体中
{
...
}
正确方法:
void Foo(int x, int y=0, int z=0);
错误方法:
void Foo(int x=0, int y, int z=0);
宏代码与内联函数区别
语言支持关系:
C 宏代码
C++ 宏代码 内联函数
宏代码本身不是函数,但使用起来象函数.预处理器用复制宏代码的方式代替函数调用,省去了参数压栈、生成汇编语言的CALL调用、返回参数、执行return 等过程,从而提高了速度.使用宏代码最大的缺点是容易出错,预处理器在复制宏代码时常常产生意想不到的边际效应。预处理器不能进行类型安全检查,或者进行自动类型转换。
类型安全特指内存类型安全,即类型安全代码只访问被授权可以访问的内存位置。如果代码以任意偏移量访问内存,该偏移量超出了属于该对象的公开字段的内存范围,则它就不是类型安全的代码。楼上说的指针或是函数指针都不属于类型安全代码。
对于任何内联函数,编译器在符号表里放入函数的声明(包括名字、参数类型、返回值类型).如果编译器没有发现内联函数存在错误,那么该函数的代码也被放入符号表里.在调用一个内联函数时,编译器首先检查调用是否正确(进行类型安全检查,或者进行自动类型转换,当然对所有的函数都一样).如果正确,内联函数的代码就会直接替换函数调用,于是省去了函数调用的开销.这个过程与预处理有显著的不同,因为预处理器不能进行类型安全检查,或者进行自动类型转换.假如内联函数是成员函数,对象的地址(this)会被放在合适的地方,这也是预处理器办不到的。
内联函数使用方法:
关键字inline 必须与函数定义体放在一起才能使函数成为内联,仅将inline 放在函数声明前面不起任何作用.
正确使用方法:
void Foo(int x, int y);
inline void Foo(int x, int y) // inline 与函数定义体放在一起
{
…
}
错误使用方法:
inline void Foo(int x, int y); // inline 仅与函数声明放在一起
void Foo(int x, int y)
{
…
}
6、构造和析构的次序
构造从类层次的最根处开始,在每一层中,首先调用基类的构造函数,然后调用成员对象的构造函数.析构则严格按照与构造相反的次序执行,该次序是唯一的,否则编译器将无法自动执行析构过程。
String函数定义
class String
{
public:
String(const char *str = NULL); // 普通构造函数
String(const String &other); // 拷贝构造函数
~ String(void); // 析构函数
String & operate =(const String &other); // 赋值函数
private:
char *m_data; // 用于保存字符串
};
// String 的析构函数
String::~String(void)
{
delete [] m_data;// 由于m_data 是内部数据类型,也可以写成delete m_data;
}
// String 的普通构造函数
String::String(const char *str)
{
if(str==NULL)
{
m_data = new char[1]; // 若能加NULL 判断则更好
*m_data = '\0';
}
else
{
int length = strlen(str);
m_data = new char[length+1]; // 若能加NULL 判断则更好
strcpy(m_data, str);
}
}
// 拷贝构造函数
String::String(const String &other)
{
int length = strlen(other.m_data);
m_data = new char[length+1]; // 若能加NULL 判断则更好
strcpy(m_data, other.m_data);
}
// 赋值函数
String & String::operate =(const String &other)
{
// (1) 检查自赋值
if(this == &other)
return *this;
// (2) 释放原有的内存资源
delete [] m_data;
// (3)分配新的内存资源,并复制内容
int length = strlen(other.m_data);
m_data = new char[length+1]; // 若能加NULL 判断则更好
strcpy(m_data, other.m_data);
// (4)返回本对象的引用
return *this;
}
2. 什么是“引用”?申明和使用“引用”要注意哪些问题?
答:引用就是某个目标变量的“别名”(alias),对应用的操作与对变量直接操作效果完全相同。申明一个引用的时候,切记要对其进行初始化。引用声明完毕后,相当于目标变量名有两个名称,即该目标原名称和引用名,不能再把该引用名作为其他变量名的别名。声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。不能建立数组的引用。
11. 已知strcpy的函数原型:char *strcpy(char *strDest, const char *strSrc)其中strDest 是目的字符串,strSrc 是源字符串。不调用C++/C 的字符串库函数,请编写函数 strcpy。
答案:
char *strcpy(char *strDest, constchar *strSrc)//传入的是两个指针的拷贝,但是指向的是原来的内容
{
if ( strDest == NULL || strSrc ==NULL)
return NULL ;
if ( strDest == strSrc)
return strDest ;
char *tempptr = strDest ;
while( (*strDest++ = *strSrc++) !=‘\0’);
return tempptr ;
}
12. 已知String类定义如下:
class String
{
public:
String(const char *str = NULL); // 通用构造函数
String(const String &another);// 拷贝构造函数
~ String(); // 析构函数
String & operater =(constString &rhs); // 赋值函数
private:
char *m_data; // 用于保存字符串
};
尝试写出类的成员函数实现。
答案:
String::String(const char *str)
{
if ( str == NULL ) //strlen在参数为NULL时会抛异常才会有这步判断
{
m_data = new char[1] ;
m_data[0] = '\0' ;
}
else
{
m_data = new char[strlen(str) + 1];
strcpy(m_data,str);
}
}
String::String(const String&another)
{
m_data = new char[strlen(another.m_data) + 1];
strcpy(m_data,other.m_data);
}
String& String::operator=(const String &rhs)
{
if ( this == &rhs)
return *this ;
delete []m_data; //删除原来的数据,新开一块内存
m_data = new char[strlen(rhs.m_data) + 1];
strcpy(m_data,rhs.m_data);
return *this ;
}
String::~String()
{
delete[]m_data ;
}
由于c规定,用Int表示布尔型时,任何非0的int都表示true,所以,只要用户输入的是1、2、3、4、5、6、7...中的一种,循环就会继续。。然后通过switch对m进行判断,执行对应操作。。当m=0时,循环结束,退出。
13. #include<file.h> 与 #include "file.h"的区别?
答:前者是从Standard Library的路径寻找和引用file.h,而后者是从当前工作路径搜寻并引用file.h。
14.在C++程序中调用被C 编译器编译后的函数,为什么要加extern “C”?
首先,作为extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。
通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。例如,如果模块B欲引用该模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在连接阶段中从模块A编译生成的目标代码中找到此函数
extern "C"是连接申明(linkage declaration),被extern "C"修饰的变量和函数是按照C语言方式编译和连接的,来看看C++中对类似C的函数是怎样编译的:
作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在符号库中的名字与C语言的不同。例如,假设某个函数的原型为:
void foo( int x, int y );
该函数被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字(不同的编译器可能生成的名字不同,但是都采用了相同的机制,生成的新名字称为“mangledname”)。
_foo_int_int 这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机制来实现函数重载的。例如,在C++中,函数void foo( int x,int y )与void foo(int x, float y )编译生成的符号是不相同的,后者为_foo_int_float。
同 样地,C++中的变量除支持局部变量外,还支持类成员变量和全局变量。用户所编写程序的类成员变量可能与全局变量同名,我们以"."来区分。而本质上,编 译器在进行编译时,与函数的处理相似,也为类中的变量取了一个独一无二的名字,这个名字与用户程序中同名的全局变量名字不同。
4.extern "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( intx, int y );
#endif
//C++实现文件 cppExample.cpp
#include "cppExample.h"
int add( int x, int y )
{
return x + y;
}
/* C实现文件 cFile.c
/* 这样会编译出错:#include "cExample.h"*/
externint add( int x, int y );
int main( int argc, char* argv[] )
{
add( 2, 3 );
return 0;
}
所 以,可以用一句话概括extern “C”这个声明的真实目的(任何语言中的任何语法特性的诞生都不是随意而为的,来源于真实世界的需求驱动。我们在思考问题时,不能只停留在这个语言是怎么 做的,还要问一问它为什么要这么做,动机是什么,这样我们可以更深入地理解许多问题):实现C++与C及其它语言的混合编程。
16. 关联、聚合(Aggregation)以及组合(Composition)的区别?
涉及到UML中的一些概念:关联是表示两个类的一般性联系,比如“学生”和“老师”就是一种关联关系;聚合表示has-a的关系,是一种相对松散的关系,聚合类不需要对被聚合类负责,如下图所示,用空的菱形表示聚合关系:
17. 重载(overload)和重写(overried,有的书也叫做“覆盖”)的区别?
常考的题目。从定义上来说:
重载:是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。
重写:是指子类重新定义复类虚函数的方法。
从实现原理上来说:
重载: 编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数(至少对于编译器来说是这样的)。如,有两个同名函数:function func(p:integer):integer;和function func(p:string):integer;。那么编译器做过修饰后的函数名称可能是这样的:int_func、str_func。对于这两个函数的 调用,在编译器间就已经确定了,是静态的。也就是说,它们的地址在编译期就绑定了(早绑定),因此,重载和多态无关!
重写:和多态真正相关。当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态的调用属于子类的该函数,这样的函数调用在编译期间是无法确定的(调用的子类的虚函数的地址无法给出)。因此,这样的函数地址是在运行期绑定的
18. #define DOUBLE(x) x+x ,i = 5*DOUBLE(5); i 是多少?
答案:i 为30。
19. 有哪几种情况只能用intialization list 而不能用assignment?
答案:当类中含有const、reference 成员变量;基类的构造函数都需要初始化表。
20. C++是不是类型安全的?
答案:不是。两个不同类型的指针之间可以强制转换(用reinterpret cast)。C#是类型安全的。
21. main 函数执行以前,还会执行什么代码?
答案:全局对象的构造函数会在main 函数之前执行。
22.struct 和 class 的区别
答案:struct 的成员默认是公有的,而类的成员默认是私有的。struct 和 class 在其他方面是功能相当的。
23.分别写出BOOL,int,float,指针类型的变量a 与“零”的比较语句。
答案:
BOOL: if ( !a ) or if(a)
int : if ( a == 0)
float: const EXPRESSION EXP = 0.000001
if ( a < EXP&& a >-EXP)
pointer : if ( a != NULL) or if(a == NULL)
24. 比较C++中的4种类型转换方式?重点是static_cast, dynamic_cast和reinterpret_cast的区别和应用。
强制类型转换运算符:C++有四种强制类型转换符,分别是dynamic_cast,const_cast,static_cast,reinterpret_cast。其中dynamic_cast与运行时类型转换密切相关,在这里我们先介绍dynamic_cast,其他三种在后面介绍。
6.1、dynamic_cast强制转换运算符:该转换符用于将一个指向派生类的基类指针或引用转换为派生类的指针或引用,注意dynamic_cast转换符只能用于含有虚函数的类,其表达式为dynamic_cast<类型>(表达式),其中的类型是指把表达式要转换成的目标类型,比如含有虚函数的基类B和从基类B派生出的派生类D,
则B *pb; D *pd, md; pb=&md; pd=dynamic<D*>(pb);
最后一条语句表示把指向派生类D的基类指针pb转换为派生类D的指针,然后将这个指针赋给派生类D的指针pd,有人可能会觉得这样做没有意义,既然指针pd要指向派生类为什么不pd=&md;这样做更直接呢?有些时候我们需要强制转换,比如如果指向派生类的基类指针B想访问派生类D中的除虚函数之外的成员时就需要把该指针转换为指向派生类D的指针,以达到访问派生类D中特有的成员的目的,比如派生类D中含有特有的成员函数g(),这时可以这样来访问该成员dynamic_cast<D*>(pb)->g();因为dynamic_cast转换后的结果是一个指向派生类的指针,所以可以这样访问派生类中特有的成员。但是该语句不影响原来的指针的类型。如果单独使用该指针仍然不能访问派生类中特有的成员。一般情况下不推见这样使用dynamic_cast转换符,因为dynamic_cast的转换并不会总是成功的,具体情况在后面介绍。
6.2、dynamic_cast的注意事项:dynamic_cast转换符只能用于指针或者引用。dynamic_cast转换符只能用于含有虚函数的类。dynamic_cast转换操作符在执行类型转换时首先将检查能否成功转换,如果能成功转换则转换之,如果转换失败,如果是指针则反回一个0值,如果是转换的是引用,则抛出一个bad_cast异常,所以在使用dynamic_cast转换之间应使用if语句对其转换成功与否进行测试,比如pd=dynamic_cast<D*>(pb); if(pd){…}else{…},或者这样测试if(dynamic_cast<D*>(pb)){…}else{…}。
6.3、const_cast操作符:其表达式为const_cast<类型>(表达式),其中类型指要把表达式转换为的目标类型。该操作符用于改变const和volatile,const_cast最常用的用途就是删除const属性,如果某个变量在大多数时候是常量,而在某个时候又是需要修改的,这时就可以使用const_cast操作符了。const_cast操作符不能改变类型的其他方面,他只能改变const或volatile,即const_cast不能把int改变为double,但可以把const int改变为int。const_cast只能用于指针或引用。const_cast的用法举例比如:int a=3; const int *b=&a; int* c; c=const_cast<int*>(b); *c=4; cout<<a<<*c;这时输出两个4,如果不使用const_cast转换符则常量指针*c的值是不能改变的,在这里使用const_cast操作符,通过指针b就能改变常量指针和变量a的值。
6.4、static_cast操作符:该操作符用于非多态类型的转换,任何标准转换都可以使用他,即static_cast可以把int转换为double,但不能把两个不相关的类对象进行转换,比如类A不能转换为一个不相关的类B类型。static_cast本质上是传统c语言强制转换的替代品。
6.5、reinterpret_cast操作符:该操作符用于将一种类型转换为另一种不同的类型,比如可以把一个整型转换为一个指针,或把一个指针转换为一个整型,因此使用该操作符的危险性较高,一般不应使用该操作符。
25 *(ptr++)+=12;
相当于 *ptr=*ptr+12;ptr++
char b= ~a>>4
>> 的优先级要高于 ~(取反)
26 关于printf参数的问题:
printf(“%d,%d\n”,*ptr,*(++ptr));
参数的传递顺序: 右->左 压栈 故先计算*(++ptr),将结果再压入栈。
27 关于数据类型转换:
(1) 混合类型的算术表达式:
最宽的数据类型成为目标转换类型;
Int ival =3;
Doule dval=3.14159;
Ival+dval;
(2) 一种类型的表达式赋值给另一种类型的对象;
int *ptr=0; ival=dval;
C++存储区域概念详解
1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由系统回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 程序结束后由系统释放
4、文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放
5、程序代码区—存放函数体的二进制代码。
C++存储区域中的局部变量可以用类型相符的任意表达式来初始化
而全局变量只能用常量表达式(Constant Expression)初始化。
例如,全局变量pi这样初始化是合法的:
1. double pi = 3.14 + 0.0016;
但这样初始化是不合法的:
2. double pi = acos(-1.0);
如果全局变量在定义时不初始化则初始值是0,如果局部变量在定义时不初始化则初始值是不确定的。
所以,C++存储区域中的局部变量在使用之前一定要先赋值,如果基于一个不确定的值做后续计算肯定会引入Bug。
问题(19):运行下图中C代码,输出的结果是什么?
int _tmain(int argc, _TCHAR* argv[])
{
char str1[] = "hello world";
char str2[] = "hello world";
char* str3 = "hello world";
char* str4 = "hello world";
if(str1 == str2)
printf("str1 and str2 aresame.\n");
else
printf("str1 and str2 are not same.\n");
if(str3 == str4)
printf("str3 and str4 are same.\n");
else
printf("str3 and str4 are not same.\n");
return 0;
}
答案:输出两行。第一行是str1and str2 are not same,第二行是str3 and str4 are same。
首先明确一点:数组名字代表指针
str1和str2是两个字符串数组。我们会为它们分配两个长度为12个字节的空间,并把"hello world"的内容分别拷贝到数组中去。这是两个初始地址不同的数组,比较的是地址而不是内容,你的明白?因此比较str1和str2的值,会不相同。str3和str4是两个指针,我们无需为它们分配内存以存储字符串的内容,而只需要把它们指向"hello world“在内存中的地址就可以了。由于"helloworld”是常量字符串,它在内存中只有一个拷贝,因此str3和str4指向的是同一个地址。因此比较str3和str4的值,会是相同的。
题目(12):运行下图中的C++代码,输出是什么?
#include <iostream>
class A
{
private:
int n1;
int n2;
public:
A(): n2(0), n1(n2 + 2)//依照声明顺序进行初始化
{
}
void Print()
{
std::cout << "n1:" << n1 << ", n2: " << n2 << std::endl;
}
};
int _tmain(int argc, _TCHAR* argv[])
{
A a;
a.Print();
return 0;
}
答案:输出n1是一个随机的数字,n2为0。在C++中,成员变量的初始化顺序与变量在类型中的申明顺序相同,而与它们在构造函数的初始化列表中的顺序无关。因此在这道题中,会首先初始化n1,而初始n1的参数n2还没有初始化,是一个随机值,因此n1就是一个随机值。初始化n2时,根据参数0对其初始化,故n2=0。
题目(13):编译运行下图中的C++代码,结果是什么?(A)编译错误;(B)编译成功,运行时程序崩溃;(C)编译运行正常,输出10。请选择正确答案并分析原因。
#include <iostream>
class A
{
private:
int value;
public:
A(int n)
{
value = n;
}
A(A other)
{
value = other.value;
}
void Print()
{
std::cout << value<< std::endl;
}
};
int _tmain(int argc, _TCHAR* argv[])
{
A a = 10;
A b = a;
b.Print();
return 0;
}
答案:编译错误。在复制构造函数中传入的参数是A的一个实例。由于是传值,把形参拷贝到实参会调用复制构造函数。因此如果允许复制构造函数传值,那么会形成永无休止的递归并造成栈溢出。因此C++的标准不允许复制构造函数传值参数,而必须是传引用或者常量引用。在Visual Studio和GCC中,都将编译出错。如果把拷贝构造函数参数的 &去掉,在调用拷贝构造函数的时候就会为其参数生成一个临时对象,生成临时对象需要调用该类的拷贝构造函数...,因此会产生无穷递归.
#include <iostream>
using namespace std;
class Test
{
public:
Test(int temp)
{
p1=temp;
}
protected:
int p1;
};
void main()
{
Test a(99);
Test b=a;
}
普通对象和类对象同为对象,他们之间的特性有相似之处也有不同之处,类对象内部存在成员变量,而普通对象是没有的,当同样的复制方法发生在不同的对象上的时候,那么系统对他们进行的操作也是不一样的,就类对象而言,相同类型的类对象是通过拷贝构造函数来完成整个复制过程的,在上面的代码中,我们并没有看到拷贝构造函数,同样完成了复制工作,这又是为什么呢?因为当一个类没有自定义的拷贝构造函数的时候系统会自动提供一个默认的拷贝构造函数,来完成复制工作。
#include <iostream>
using namespace std;
class Test
{
public:
Test(int temp)
{
p1=temp;
}
Test(Test &c_t)//这里就是自定义的拷贝构造函数
{
cout<<"进入copy构造函数"<<endl;
p1=c_t.p1;//这句如果去掉就不能完成复制工作了,此句复制过程的核心语句
}
public:
int p1;
};
void main()
{
Test a(99);
Test b=a;
cout<<b.p1;
cin.get();
}
上面代码中的Test(Test &c_t)就是我们自定义的拷贝构造函数,拷贝构造函数的名称必须与类名称一致,函数的形式参数是本类型的一个引用变量,且必须是引用。
当用一个已经初始化过了的自定义类类型对象去初始化另一个新构造的对象的时候,拷贝构造函数就会被自动调用,如果你没有自定义拷贝构造函数的时候系统将会提供给一个默认的拷贝构造函数来完成这个过程,上面代码的复制核心语句就是通过Test(Test &c_t)拷贝构造函数内的p1=c_t.p1;语句完成的。如果取掉这句代码,那么b对象的p1属性将得到一个未知的随机值;
题目(14):运行下图中的C++代码,输出是什么?
int SizeOf(char pString[])
{
return sizeof(pString);
}
int _tmain(int argc, _TCHAR* argv[])
{
char* pString1 = "google";
int size1 = sizeof(pString1);
int size2 = sizeof(*pString1);
char pString2[100] = "google";
int size3 = sizeof(pString2);
int size4 = SizeOf(pString2);
printf("%d, %d, %d, %d", size1, size2, size3, size4);
return 0;
}
答案:4, 1, 100, 4。pString1是一个指针。在32位机器上,任意指针都占4个字节的空间。*pString1是字符串pString1的第一个字符。一个字符占一个字节。pString2是一个数组,sizeof(pString2)是求数组的大小。这个数组包含100个字符,因此大小是100个字节。而在函数SizeOf中,虽然传入的参数是一个字符数组,当数组作为函数的参数进行传递时,数组就自动退化为同类型的指针。
题目(15):运行下图中代码,输出的结果是什么?这段代码有什么问题?
#include <iostream>
class A
{
public:
A()
{ std::cout << "Ais created." << std::endl; }
~A()
{ std::cout << "Ais deleted." << std::endl; }
};
class B : public A
{
public:
B()
{ std::cout << "Bis created." << std::endl; }
~B()
{ std::cout << "Bis deleted." << std::endl; }
};
int _tmain(int argc, _TCHAR* argv[])
{
A* pA = new B();
delete pA;
return 0;
}
答案:输出三行,分别是:Ais created. B is created. A is deleted。用new创建B时,回调用B的构造函数。在调用B的构造函数的时候,会先调用A的构造函数。因此先输出A is created. B iscreated.
接下来运行delete语句时,会调用析构函数。由于pA被声明成类型A的指针,同时基类A的析构函数没有标上virtual,因此只有A的析构函数被调用到,而不会调用B的析构函数。
由于pA实际上是指向一个B的实例的指针,但在析构的时候只调用了基类A的析构函数,却没有调用B的析构函数。这就是一个问题。如果在类型B中创建了一些资源,比如文件句柄、内存等,在这种情况下都得不到释放,从而导致资源泄漏。
问题(16):运行如下的C++代码,输出是什么?
class A
{
public:
virtual void Fun(int number = 10)
{
std::cout << "A::Fun with number " << number;
}
};
class B: public A
{
public:
virtual void Fun(int number = 20)
{
std::cout << "B::Fun with number " << number;
}
};
int main()
{
Bb;
A&a = b;
a.Fun();
}
答案:输出B::Funwith number 10。由于a是一个指向B实例的引用,因此在运行的时候会调用B::Fun。但缺省参数是在编译期决定的。在编译的时候,编译器只知道a是一个类型a的引用,具体指向什么类型在编译期是不能确定的,因此会按照A::Fun的声明把缺省参数number设为10。这一题的关键在于理解确定缺省参数的值是在编译的时候,但确定引用、指针的虚函数调用哪个类型的函数是在运行的时候。
基本 C++基础知识:
1.for循环语句:
for(int i=1;i<10;i++)
{
Cout<<”hello”;
}
先执行 i=1;在执行i<10;然后执行Cout<<”hello”;;最后执行i++;
for循环的三个语句都可以省略,但是分号不能省。
2.continue语句
把 100~200之间的不能被3整除的数输出
for(int n=100;n<=200;++n)
{
If(n%3==0)
Continue;
}
与break相反。
3.关于补码
正数不用动,前面补0即可。
负数:各位数取反加一
01111110-------正数
10001101-------负数
一定长度的二进制补码中有一般是正数,一半是负数。
由于 补码 正数最高位必须是0,负数最低位必须是1,所以决定了如下:
8位二进制补码中,128个数最高位是0,另外128个是负数
所能表示的最大正数是 127,即01111111;最小非负数是0,即00000000;
能表示的绝对值最大的负整数是-128;
补码经典实例:
unsigned short A = 10;
printf("~A = %u\n", ~A);
char c=128;
printf("c=%d\n",c);
输出多少?并分析过程
这两道题都是在考察二进制向int或uint转换时的最高位处理。
2^32=4294967296,
A=10,为无符号型,转换为二进制为0000 0000 0000 0000 0000 0000 0000 1010
所以~A的二进制位1111 1111 1111 1111 1111 1111 1111 0101即0xFFFFFFF5,如果转换为符号整型的话则为-11,因为输出的是无符号整型,无符号整型的范围为0~4294967295,而0xFFFFFFF5转换为无符号十进制整型为4294967285
第二题,发生溢出,因为有符号字符型其范围为-128~127
127用二进制表示为:0111 1111,128表示为1000 0000,这里发生溢出,因为第一位为1,为符号位,表示负数,即-128
只有有符号的整数才有原码、反码和补码的!其他的类型一概没有。故:
~A的二进制位1111 1111 1111 1111 1111 1111 1111 0101输出无符号型,没有补码表示,故是什么就输出什么。
由于 补码 正数最高位必须是0,负数最低位必须是1,所以决定了如下:
8位二进制补码中,128个数最高位是0,另外128个是负数
所能表示的最大正数是 127,即01111111;最小非负数是0,即00000000;
能表示的绝对值最大的负整数是-128;
int a=12345678901234567890123456789
C++ 标准告诉我们,当正数的表示范围在整型的表示范围内时,任何编译器的理解都是一致的,超过了表示范围,不同的编译器有不同的理解。
余数的正负性取决于被除数的正负性。
11/(-5)= -2;11%(-5)=1;
4.枚举类型
枚举是对整型区间的自定义的类型,用户需要为区间中的值取名。
例如:
enum Week{Mon,Tue,Wed,Thu,Fri,Sat,Sun};
默认时,第一个名称对应正数0;相当于定义宏一样。
Color color =ReD;
Color =5;//错误,不能直接赋值,5是非枚举值。
可以强制转化:color=Color(5);
枚举符号一旦定义则不能改变,可以当常量使用,常量的赋值,枚举不用。
5.bool型
整数 0和 1 两个值构成了bool 型的表示范围。相当于:
enum bool {false,true};
任何非0整数 或者 非整数的其他类型,只要非 0 。其值也是 1;
bool 型的输出可以改变,默认为整数 1和 0,如果要输出true和false ,可以用控制符:boolalpha;
cout<<boolalpha<<d<<endl;
7.数组定义是具有编译确定意义的操作。他分配固定大小的空间,因此元素个数必须是编译时就能确定的。
int n=100;
int a
;//出错,元素个数必须是常量。
const int n=100;
int a
;//正确,常量,编译时就能确定。
int a; // 它既是声明,同时又是定义即在声明a为int类型数据时,就已经为a分配了存储空间,这种情况称为定义或定义性声明。
extern int a; // 它只是声明不是定义
由于变量a是在别的代码中定义的,因此不需要为a分配空间,这种情况称之为声明或引用性声明。
综上所述,我们可以将为变量分配存储空间的声明称之为“定义”,而将不需要为变量分配存储空间的称之为“声明”,即非定义性质的声明。
8.一个 * 只能修饰一个指针。int * p,q;表示 p是指针,q是整形变量。
int max(int x,int y)
{
return x>y? x : y;
}
函数指针:
int(*p)(int ,int)=&max;
比如说int a,b;
9.前增量和后增量的区别:
i++ 是后增量,在使用过i后i增1; ++i 是前增量,在使用i前使i增1; 例如,设k=0,i=1; 那么,k=i++所产生的操作就是: k=i;i+=1; 此时k=1,i=2; 而 如果用k=++i;那产生的操作就是: i+=1;k=i;此时k=2,i=2;
a=++b;等价于:b+=1;a=(return b);
a=b++;等价于:int temp=b;b+=1;a=return(temp);
负载测试和强度测试,都属于性能测试的子集。下面举个跑步的例子进行解释 性能测试,表示在一个给定的基准下,能执行的最好情况。例如,在没有负重的情况下,你跑100米需要花多少时间(这边,没有负重是基准)? 负载测试,也是性能测试,但是他是在不同的负载下的。对于刚才那个例子,如果扩展为:在50公斤、100公斤……等情况下,你跑100米需要花多少时间? 强度测试,是在强度情况下的性能测试。对于刚才那个例子,如果改为:在一阵强风的情况下,你在负重或没有负重的情况下,跑100米需要花多少时间?
接口和抽象类的区别 转自:http://blog.csdn.net/limfungsuen/article/details/2986384
四、抽象类和接口的使用:
抽象类和接口有什么区别?
1.接口是公开的,里面不能有私有的方法或变量,是用于让别人使用的,而抽象类是可以有私有方法或私有变量的。
2.另外,实现接口的一定要实现接口里定义的所有方法,而实现抽象类可以有选择地重写需要用到的方法。
3. 抽象类的方法的访问权限可以多种,接口的方法的访问权限只能是public;抽象类的子类只能继承一个抽象类,而实现接口的类可以同时实现多个接口。
C++ 与C的优缺点比较:
C++可以看成是C的扩展,C++相对于c来说最大的优势是其支持面向对象的机制,在编写较大的程序的时候相对于c是有很大优势的,程序结构清晰,开发效率高,开发周期短,但效率不如c。
相关文章推荐
- 实例解析 C/C++ 疑难问题(一)
- 【C++深度剖析教程15】经典问题解析之关于string的疑问
- [C++]有关深复制与copy constructor的一些问题与实例
- C、C++动态分配函数暨几种参数传递疑难解析
- 实例解析C++/CLI之头文件、内联函数与数组
- 最大子数组问题-c++代码实现及运行实例结果
- 雇佣问题(hireassistant)-c++代码实现及运行实例结果
- 最简单的0-1背包问题c++代码实例及运行结果
- C++调用python时 debug链接错误python_d.lib无法解析的问题
- 从LINQ实例解析LINQ的另类用法,解决多条件组合问题
- c_c++刁钻问题各个击破之位运算及其实例(2)
- C++中vector的用法实例解析
- 实例解析C++/CLI的“克隆”
- 实例解析C++/CLI之继承与枚举
- 《C++高级编程》之--理解C++疑难问题
- 最大子数组问题-暴力求解-c++代码实现及运行实例结果
- c++解析tcp头部遇到的大小端转换问题
- 雇佣问题(hireassistant)-c++代码实现及运行实例结果
- 雇佣问题原址排列给定数组(randomize In Place)-c++代码实现及运行实例结果
- C++ XML文件解析实例