C++笔记

来源:互联网 发布:多功能乐器软件 编辑:程序博客网 时间:2024/06/10 16:41

















#、构造函数对每个成员变量的初始化顺序取决于成员变量在类中的说明顺序,初始化顺序是按照类中每个成员变量的说明顺序。析构函数正好与构造函数相反。

#、当我们声明一个变量或一个对象,系统就会根据它们的类型自动的在栈中为每个变量开辟内存空间,以保证数值被合理地存放。(栈中空间比较小,据说只有2MB)

#、所谓浅拷贝,就是仅仅将一个对象的成员指针复制给另一个对象,因此两个对象的成员指针都指向同一块内存区域。(又叫成员拷贝)简单来说,浅拷贝就是——地址拷贝。

#、所谓深拷贝,必须创建自己的复制构造函数,并且在函数中为我们的成员变量分配内存,这样,在分配完内存后,旧对象的成员变量就可以复制到新的内存区域中,两个对象的成员变量都各自拥有自己的内存区域,一个对象在析构后不会在影响到另一个。简单来说,深拷贝就是——值拷贝。

#、一般来说,只有派生类的对象可以赋值给基类的对象。另外,派生类的对象还可以对基类的对象进行指针变量的赋值操作。但是换过来则不行,也就是说基类对象的地址不可以赋给派生类的指针和引用。

#、只有在使用指针或者引用的方式来调用虚函数时,虚函数才能起到运行时的多态作用。被继承的虚函数仍为虚函数。

#、每个对象创建虚函数时,对象都记录这个虚函数,因此编译器建立了一个叫做T表的虚函数表。每个对象都有一个指向该表的指针,叫做虚表指针。该指针用来指向虚函数表。虚函数表也有一个指针指向该对象,当创建派生类对象的基类部分时,该对象的指针就自动初始化为指向虚函数表的正确部分。当调用派生类对象的构造函数时,这个对象就会添加到虚函数表中去,并且将指针指向该对象的重载函数。当使用指向基类的指针时,根据对象的实际类型,将该对象的指针继续指向正确的函数。

#、设置虚函数须注意: 
            1:只有类的成员函数才能说明为虚函数; 
            2:静态成员函数不能是虚函数; 
            3:内联函数不能为虚函数; 
            4:构造函数不能是虚函数; 
            5:析构函数可以是虚函数,而且通常声明为虚函数。

常见的不不能声明为虚函数的有:普通函数(非成员函数);静态成员函数;内联成员函数;构造函数;友元函数。

1.为什么C++不支持普通函数为虚函数?

普通函数(非成员函数)只能被overload,不能被override,声明为虚函数也没有什么意思,因此编译器会在编译时邦定函数。

2.为什么C++不支持构造函数为虚函数?

这个原因很简单,主要是从语义上考虑,所以不支持。因为构造函数本来就是为了明确初始化对象成员才产生的,然而virtual function主要是为了再不完全了解细节的情况下也能正确处理对象。另外,virtual函数是在不同类型的对象产生不同的动作,现在对象还没有产生,如何使用virtual函数来完成你想完成的动作。(这不就是典型的悖论)

3.为什么C++不支持内联成员函数为虚函数?

其实很简单,那内联函数就是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的。(再说了,inline函数在编译时被展开,虚函数在运行时才能动态的邦定函数)

4.为什么C++不支持静态成员函数为虚函数?

这也很简单,静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码,他不归某个具体对象所有,所以他也没有要动态邦定的必要性。

5.为什么C++不支持友元函数为虚函数?

因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。




#、在堆中声明一个数组:

               area *one = new area[1000];

   在堆中删除一个数组:

                delete[] one;

        []表示删除一个数组。如果忘记了输入[],那么删除的只是one[0],该错         误带来的后果就是内存泄漏。

2. 什么是“引用”?申明和使用“引用”要注意哪些问题?

答:引用就是某个目标变量的“别名”(alias),对应用的操作与对变量直接操作效果完全相同。申明一个引用的时候,切记要对其进行初始化。引用声明完毕后,相当于目标变量名有两个名称,即该目标原名称和引用名,不能再把该引用名作为其他变量名的别名。声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。不能建立数组的引用。

 

3. 将“引用”作为函数参数有哪些特点?

(1)传递引用给函数与传递指针的效果是一样的。这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。

(2)使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。

(3)使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用"*指针变量名"的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。

//下面关于“联合”的题目的输出?#include <stdio.h>union un{int i;char x[2];}a;int main(){a.x[0] =10; a.x[1] =1;printf("%d\n",a.i);return 0;}//答案:266 (低位低地址,高位高地址,内存占用情况是0x010A)
#include <stdio.h>int 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); return 0;}//答案: AB   (0x41对应'A',是低位;0x42对应'B',是高位) //      6261 (number.i和number.half共用一块地址空间)

/*编写strcpy函数(10分)已知strcpy函数的原型是    char *strcpy(char *strDest, const char *strSrc);    其中strDest是目的字符串,strSrc是源字符串。(1)不调用C++/C的字符串库函数,请编写函数 strcpy(2)strcpy能把strSrc的内容复制到strDest,为什么还要char * 类型的返回值?答:为了 实现链式表达式。                            // 2分例如    int length = strlen( strcpy( strDest, “hello world”) );*/#include <assert.h>#include <stdio.h>char*strcpy(char*strDest, constchar*strSrc){    assert((strDest!=NULL) && (strSrc !=NULL));        // 2分char* address = strDest;                          // 2分while( (*strDest++=*strSrc++) !='\0' )       // 2分       NULL;     return address ;                                   // 2分}

//另外strlen函数#include<stdio.h>#include<assert.h> int strlen( constchar*str )  // 输入参数const{    assert( str != NULL );  // 断言字符串地址非0int len = 0;    while( (*str++) !='\0' )     {         len++;     }     return len;}


#、已知String类定义如下:

class String{public:String(const char *str = NULL); // 通用构造函数String(const String &another); // 拷贝构造函数~String(); // 析构函数String& operator =(const String &rhs); // 赋值函数private:char* m_data; // 用于保存字符串};

尝试写出类的成员函数实现。

#include <iostream>#include <string.h>using namespace std;class String{public:String(const char *str = NULL); // 通用构造函数String(const String &another); // 拷贝构造函数~String(); // 析构函数String& operator =(const String &rhs); // 赋值函数private:char* m_data; // 用于保存字符串};//错误写法:String::String(const char *str = NULL )//原因:构造函数的声明和定义不能同时有缺省参数,String::String(const char *str ) // 通用构造函数{if( NULL == str)//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];stpcpy(m_data,another.m_data);}String::~String() // 析构函数{delete []m_data;}//错误写法:String::String& operator=(const String &rhs)String& String::operator=(const String &rhs) // 赋值函数{if( this == &rhs )//因为这里的this是指针,所以&的作用是取地址return *this;//返回一个*this的引用,这样是为了连续赋值的时候的效率考虑的delete []m_data;//释放原来的数据m_data = new char[strlen(rhs.m_data)+1];stpcpy(m_data,rhs.m_data);return *this;}int main(){String test;return 0;}

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

转载自:http://www.cnblogs.com/fangyukuan/archive/2010/09/18/1829871.html

15.在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之类的名字(不同的编译器可能生成的名字不同,但是都采用了相同的机制,生成的新名字称为“mangled name”)。

_foo_int_int 这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机制来实现函数重载的。例如,在C++中,函数void foo( int x, int y )与void foo( int x, float y )编译生成的符号是不相同的,后者为_foo_int_float。

同 样地,C++中的变量除支持局部变量外,还支持类成员变量和全局变量。用户所编写程序的类成员变量可能与全局变量同名,我们以"."来区分。而本质上,编译器在进行编译时,与函数的处理相似,也为类中的变量取了一个独一无二的名字,这个名字与用户程序中同名的全局变量名字不同。

未加extern "C"声明时的连接方式

假设在C++中,模块A的头文件如下:

// 模块A头文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
int foo( int x, int y );
#endif  

 

在模块B中引用该函数:

// 模块B实现文件 moduleB.cpp
#include "moduleA.h"
foo(
2,3);

实际上,在连接阶段,连接器会从模块A生成的目标文件moduleA.obj中寻找_foo_int_int这样的符号!

 

加extern "C"声明后的编译和连接方式

加extern "C"声明后,模块A的头文件变为:

// 模块A头文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
extern"C"int foo( int x, int y );
#endif 

在模块B的实现文件中仍然调用foo( 2,3 ),其结果是:
(1)模块A编译生成foo的目标代码时,没有对其名字进行特殊处理,采用了C语言的方式;

(2)连接器在为模块B的目标代码寻找foo(2,3)调用时,寻找的是未经修改的符号名_foo。

如果在模块A中函数声明了foo为extern "C"类型,而模块B中包含的是extern int foo( int x, int y ) ,则模块B找不到模块A中的函数;反之亦然。

所以,可以用一句话概括extern “C”这个声明的真实目的(任何语言中的任何语法特性的诞生都不是随意而为的,来源于真实世界的需求驱动。我们在思考问题时,不能只停留在这个语言是怎么做的,还要问一问它为什么要这么做,动机是什么,这样我们可以更深入地理解许多问题):实现C++与C及其它语言的混合编程。  

明白了C++中extern "C"的设立动机,我们下面来具体分析extern "C"通常的使用技巧:

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
externint add(int x, inty);
#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); 
  return0;
}
复制代码

 

如果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
/* 这样会编译出错:#i nclude "cExample.h" 
*/
externint add( int x, int y );
int main( int argc, char* argv[] )
{
  add( 
23 ); 
  return0;
}
复制代码

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

 #、C++不是类型安全的。因为两个不同类型的指针之间可以强制转换(用reinterpret cast)。C#是类型安全的。

34.类成员函数的重载、覆盖和隐藏区别?
答案:
a.成员函数被重载的特征:
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。
b.覆盖是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。
c.“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)


#、程序找错

void test3(char* str1){ charstring[10]; if( strlen( str1 ) <=10 ) {  strcpy( string, str1 ); }}

如果面试者指出字符数组str1不能在数组内结束可以给3分;如果面试者指出strcpy(string,str1)调用使得从str1内存起复制到string内存起所复制的字节数具有不确定性可以给7分,在此基础上指出库函数strcpy工作方式的给10分;


再看看下面的一段程序有什么错误:

复制代码
swap( int* p1,int* p2 )
{
 
int*p;
 
*=*p1;
 
*p1 =*p2;
 
*p2 =*p;
}
复制代码

在swap函数中,p是一个“野”指针,有可能指向系统区,导致程序运行的崩溃。在VC++中DEBUG运行时提示错误“AccessViolation”。该程序应该改为:

复制代码
swap( int* p1,int* p2 )
{
 
int p;
 p 
=*p1;
 
*p1 =*p2;
 
*p2 = p;
}

试题1:分别给出BOOL,int,float,指针变量 与“零值”比较的 if 语句(假设变量名为var)

解答:

   BOOL型变量:if(!var)

   int型变量:if(var==0)

   float型变量:

   const float EPSINON = 0.00001;

   if ((x >= - EPSINON) && (x <=EPSINON)

   指针变量:  if(var==NULL)

剖析:

  考查对0值判断的“内功”,BOOL型变量的0判断完全可以写成if(var==0),而int型变量也可以写成if(!var),指针变量的判断也可以写成if(!var),上述写法虽然程序都能正确运行,但是未能清晰地表达程序的意思。 
 一般的,如果想让if判断一个变量的“真”、“假”,应直接使用if(var)、if(!var),表明其为“逻辑”判断;如果用if判断一个数值型变量(short、int、long等),应该用if(var==0),表明是与0进行“数值”上的比较;而判断指针则适宜用if(var==NULL),这是一种很好的编程习惯。

  浮点型变量并不精确,所以不可将float变量用“==”或“!=”与数字比较,应该设法转化成“>=”或“<=”形式。如果写成if(x == 0.0),则判为错,得0分。


#、编写类String的构造函数、析构函数和赋值函数,已知类String的原型为:

复制代码
class String

 
public
  String(
constchar*str = NULL); // 普通构造函数 
  String(const String &other); // 拷贝构造函数 
  ~ String(void); // 析构函数 
  String & operator =(const String &other); // 赋值函数 
 private
  
char*m_data; // 用于保存字符串 
};
  解答:
//普通构造函数
String::String(constchar*str) 
{
 
if(str==NULL) 
 {
  m_data 
=newchar[1]; // 得分点:对空字符串自动申请存放结束标志'\0'的空
  
//加分点:对m_data加NULL 判断
  *m_data ='\0'
 } 
 
else
 {
  
int length = strlen(str); 
  m_data 
=newchar[length+1]; // 若能加 NULL 判断则更好 
  strcpy(m_data, str); 
 }
}
// String的析构函数
String::~String(void
{
 delete [] m_data; 
// 或deletem_data;
}
//拷贝构造函数
String::String(const String &other)    // 得分点:输入参数为const型

 
int length = strlen(other.m_data); 
 m_data 
=newchar[length+1];     //加分点:对m_data加NULL 判断
 strcpy(m_data, other.m_data); 
}
//赋值函数
String & String::operator =(const String &other) // 得分点:输入参数为const型

 
if(this==&other)   //得分点:检查自赋值
  return*this
 delete [] m_data;     
//得分点:释放原有的内存资源
 int length = strlen( other.m_data ); 
 m_data 
=newchar[length+1];  //加分点:对m_data加NULL 判断
 strcpy( m_data, other.m_data ); 
 
return*this;         //得分点:返回本对象的引用
}
复制代码

剖析:
  能够准确无误地编写出String类的构造函数、拷贝构造函数、赋值函数和析构函数的面试者至少已经具备了C++基本功的60%以上!
  在这个类中包括了指针类成员变量m_data,当类中包括指针类成员变量时,一定要重载其拷贝构造函数、赋值函数和析构函数,这既是对C++程序员的基本要求,也是《Effective C++》中特别强调的条款。
  仔细学习这个类,特别注意加注释的得分点和加分点的意义,这样就具备了60%以上的C++基本功!


#、请说出static和const关键字尽可能多的作用

解答:
  static关键字至少有下列n个作用:
  (1)函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;
  (2)在模块内的static全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问;
  (3)在模块内的static函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内;
  (4)在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;
  (5)在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。

  const关键字至少有下列n个作用:
  (1)欲阻止一个变量被改变,可以使用const关键字。在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;
  (2)对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;
  (3)在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
  (4)对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量;
  (5)对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。例如:
       const classA operator*(const classA& a1,const classA& a2);
  operator*的返回结果必须是一个const对象。如果不是,这样的变态代码也不会编译出错:

      classA a, b, c;
      (a * b) = c; // 对a*b的结果赋值
  操作(a * b) = c显然不符合编程者的初衷,也没有任何意义。

剖析:
  惊讶吗?小小的static和const居然有这么多功能,我们能回答几个?如果只能回答1~2个,那还真得闭关再好好修炼修炼。

  这个题可以考查面试者对程序设计知识的掌握程度是初级、中级还是比较深入,没有一定的知识广度和深度,不可能对这个问题给出全面的解答。大多数人只能回答出static和const关键字的部分功能。

 




2 0
原创粉丝点击