C++ 常见问题

语言区别

C语言和C++的区别

两种不同的编程语言,又由于C++是由C语言发展而来所以有很多地方都是兼容的,就比如很多语句可以都是通用的,在很大程度上,标准C++是标准C的超集,实际上,所有C程序也可以认为是是C++程序。

最主要区别是C++具有封装、继承、多态三大特性

C是一个结构化语言,它的重点在于算法和数据结构。C程序的设计首要考虑的是如何通过一个过程,对输入(或环境条件)进行运算处理得到输出(或实现过程(事务)控制)。
而对于C++,在c的基础上增添类,首要考虑的是如何构造一个对象模型,让这个模型能够契合与之对应的问题域,这样就可以通过获取对象的状态信息得到输出或实现过程(事务)控制。

具体开发细节,两者之间有少量区别,下面简要介绍一下最重要的区别。

  1. C语言没有重载,因为编译名相同。 假设一个函数的声明如下: void function(float x,float y);
    在C语言中,编译器在编译后在库中的名字为_function
    在C++中,编译器在编译后在库中的名字为_function_float_float
    在链接时,都是找名字进行链接的。就比如以上两个函数,在C语言中两个的名字一样,就会在链接中报错。C++中它们的名字不一样,所以就不会报错。
  2. 在C中,局部变量必须在程序块的开始部分,即在所有"操作"语句之前声明,请注意,C99标准中取消了这种限制。
    在C++中,局部变量可以在一个程序块内在任何地方声明;
  3. 在C中,如果没有在函数后面的括孤内指定任何参数,这在C中就意味着对函数参数未做任何声明,该函数可能有参数,也可能没有参数,如: int
    func(); 在C++中,这样的函数声明意味着该函数没有参数,也就是说,在C++中,下面这两个函数声明具有同样的作用: int
    func(); int func(void);
    即在C++中,参数列表中的void是任选的.许多C++程序员使用它们是为了表明函数没有任何参数的,以便于他人理解程序。但是,从技术上说,void不是必须的。
  4. 在C++中,所有函数均必须被设计成原型,但这在C中只是一种选择。编程经验表明,在程序中也应该给函数采用原型设计方法。
  5. 在C与C++之间还存在一个重要而又细微的差别,即字符常数在C中被自动作为整形来处理,但在C++中则不然。
  6. 在C中,多次声明一个全局变量虽然不可取,但不算错。 在C++中,多次声明同一个全局变量会引发错误。
  7. 在C中,一个标识符可以至少31个有效的组成字符。
    在C++中,一个标识符的所有组成字符均是有效的。可是,从实用角度看,过长的标识符没有太大的用处,不仅不便于记忆,而且还会增加出现打字错误的可能性。
  8. 在C中,在程序内部调用main()函数的情形不常见,但这种做法是容许; 在C++中,这种做法是不容许的。
  9. 在C中,无法获得register型的地址; 在C++中则可以获得这种地址.
  10. 在C中,如果类型声明语句中没有指定类型名,该类型被假定成int,这种隐式转型在C99与C++中是不允许的.

C语言和Java的区别

相同点:

  1. 语法相似。 由于Java可以算是从C++发展而来的,因此Java与C语言的语法比较类似。
  2. 编程的熟练程度就是对语言程序库的掌握程度。
    从某种程度上来说,编程语言都是由语法和相应的程序库所构成,Java有自身的类库,C语言则有标准库。所谓的编程,就是使用与语法来调用和组合程序库中的函数。

区别:
1. 内存管理
在Java中,基本不用考虑内存的问题,如果想用一个对象,new一个就可以,这个过程的背后则是JRE为对象分类的一定内存,当JRE发现你不再使用这个对象的时候,他就会自动回收内存。 但是C则不同,如果你想用,你可以用malloc之类的方法申请内存,当你使用完了,你需要自己把这块内存归还回去,也就是调用free方法来释放内存。由于需要显式的归还内存,因此当一个函数需要将一块内存返回给调用者的时候,问题就比较复杂了,不如面向对象和具有内存回收功能的Java那么直观了。
对于这个问题,在C语言中,有几种解决方案:
(1) 在调用者中先分配好内存,作为参数传入到被调用的函数中
(2) 在被调用的函数中分配,使用完后在调用者中释放
(3) 在被调用函数中使用static变量,可以将该变量返回

2. 面向对象
Java的面向对象的特点很明显,而C则是一个地道的结构化语言。 Java中有一个字符串类String,通过调用String.length()就可以知道字符串的长度,但是在C语言中,则需要调用函数strlen(str)来得到字符串(字符数组)的长度。由于C不是面向对象的语言,也就没有this的概念,因此当使用一个与某个“东西”相关的函数时,就需要不厌其烦的将代表这个“东西”的变量作为参数传递进去。

3. 名称空间
Java通过包(package)来实现名称空间。
在C语言中,所有的函数都处于同一名称空间,也就是没有名称空间,因此就会很多程序提供的api接口函数都有一个前缀,例如MYSQL的mysql_init(), mysql_real_connect(), mysql_real_query()等函数名称前面的mysql_。

C++三大特性

1. 封装

突破了C语言函数的概念,封装可以隐藏实现细节,使得代码模块化。 封装可以隐藏实现细节,使得代码模块化;封装是把成员变量和成员函数封装起来,对成员变量的访问只能通过成员函数。C++类中不要留有public成员变量,应当在设计时提供一组读写其的成员函数。C#、java中都是使用属性代替了public 成员变量。
成员访问控制:

访问类型 可存取的对象
public 类的成员函数、所有类对象
protected 类的成员函数、友元(类或函数)、子类成员函数
private 类的成员函数、友元(类或函数)

2. 继承

继承可以扩展已存在的代码模块(类);达到代码重用的目的。要实现继承,可以通过“继承”(Inheritance)和“组合”(Composition)来实现。 通过继承,我们可以用原来的数据类型来定义一个新的数据类型,定义的新类型既有原来数据中的成员,也能自己添加新的成员。 我们一般把原来的数据类型称为基类或者父类,新的数据类型为派生类,或者子类。
公有继承(public)、私有继承(private)、保护继承(protected)是常用的三种继承方式。

public protected private
公有继承 public protected 不可见
私有继承 private private 不可见
保护继承 protected protected 不可见

我们几乎不使用 protected private 继承,通常使用 public 继承。当使用不同类型的继承时,遵循以下几个规则:
公有继承(public):当一个类派生自
公有
基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有保护成员来访问。
保护继承(protected): 当一个类派生自保护基类时,基类的公有保护成员将成为派生类的保护成员。
私有继承(private):当一个类派生自私有基类时,基类的公有保护成员将成为派生类的私有成员。

继承的实现方式有三类:实现继承、接口继承和可视继承。

  1. 实现继承是指使用基类的成员变量和成员函数而无需额外编码的能力;(注:private修饰的部分也会被继承,只是使用不了)
  2. 接口继承是指仅使用成员变量和成员函数的名称、但是子类必须提供实现的能力
  3. 可视继承是指子窗体(类)使用基窗体(类)的外观和实现代码的能力

在继承中虚函数、纯虚函数、普通函数的区别

1. 虚函数
C++在基类中声明一个带关键之Virtual的函数,这个函数叫虚函数。它可以在该基类的派生类中被重新定义并被赋予另外一种处理功能。
C++的虚函数主要作用是“运行时多态”,父类中提供虚函数的实现,为子类提供默认的函数实现。 子类可以重写父类的虚函数实现子类的特殊化。

2. 纯虚函数(pure virtual)
C++中包含纯虚函数的类,被称为是“抽象类”。抽象类不能使用new出对象,只有实现了这个纯虚函数的子类才能new出对象。
C++中的纯虚函数更像是“只提供申明,没有实现”,是对子类的约束,是“接口继承”。 C++中的纯虚函数也是一种“运行时多态”。

3. 普通函数(no-virtual)
普通函数是静态编译的,没有运行时多态,只会根据指针或引用的“字面值”类对象,调用自己的普通函数。
普通函数是父类为子类提供的“强制实现”。

因此,在继承关系中,子类不应该重写父类的普通函数,因为函数的调用只与类对象的字面值有关。

3. 多态

http://www.cnblogs.com/cxq0017/p/6074247.html
多态性(polymorphisn)允许将子类类型的指针赋值给父类类型的指针。可以实现接口重用,当前的框架不需改变也可以使用后来的代码。

C++的多态性用一句话概括就是:用基类的引用指向子类的对象。

在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。

多态的实现方式

1. 函数重载(编译时的多态)
必须在同一个类中进行; 子类无法重载父类的函数,父类同名函数将被名称覆盖;
重载是在编译期间根据参数类型和个数决定函数调用。

2. 函数重写 (运行时的多态)
必须发生于父类与子类之间; 并且父类与子类中的函数必须有完全相同的原型;
使用virtual声明之后能够产生多态(如果不使用virtual,那叫重定义) ; 多态是在运行期间根据具体对象的类型决定函数调用。

虚函数(virtual)内部实现机制

虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。
一个拥有virtual成员函数的类拥有一个虚函数表,而该类的每个对象都拥有一个虚指针,是一个隐式变量如_vfprt(存在于对象实例中最前面的位置),指向该类的虚函数表。运行时,通过对象自己的虚指针去索引正确的虚函数来运行。
无论指针指向哪种类型的对象,只要能够确定被调函数在虚函数中的偏移值,待运行时,能够确定具体类型,并能找到相应vptr对象指针了,就能找出真正应该调用的函数。
在这里插入图片描述
虚函数表的构造过程:
该过程是由编译器完成的,因此也可以说:虚函数替换过程发生在编译时。 可以观察到:

1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。
在这里插入图片描述
虚函数表创建时间: 在一个类构造的时候,创建这张虚函数表,而这个虚函数表是供整个类所共有的。
多重继承:
当一个类继承多个类,且多个基类都有虚函数时,子类对象中将包含多个虚函数表的指针(即多个vptr)。
在这里插入图片描述
可以观察到:

1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)

这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。

补充说明:
当类中存在虚函数,则编译器会在编译期自动的给该类生成一个函数表。并在所有该类的对像中放入一个隐式变量如_vfprt,该变量是一个指针变量,它的值指向那个类中的由编译器生成的虚函数表。

实际上每个类自己的虚函数入口都在这张表中维护,调用方法的时候会隐式的传入一个this指针,然后系统会根据this指针找到对应的_vfprt,进而找到对应的虚函数表,根据methodname找到真正方法的地址,然后才去调用这个方法,这可以叫动态绑定。

总的来说,每一个拥有virtual function的类实例化对象时,都会额外申请一块内存存储虚函数表存储所有虚函数地址,并在对象某个位置存储一个vptr指针指向该表起始地址。这个指针具体放在什么位置,虚函数表怎么组织,怎么索引各个虚函数,这些都是编译器在编译期间决定的,在不同编译环境下不见得相同。

虚函数的一些常考问题

为什么构造函数不能是虚函数?

1、从设计理念上说,构造函数不需要是虚函数;
2、从当前vptr的实现机制上说,无法实现虚的构造函数。

虚函数的调用需要虚函数表指针,而该指针存放在对象的内容空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数——构造函数了。

为什么析构函数可以为虚函数,如果不设为虚函数可能会存在什么问题?

首先析构函数可以为虚函数,而且当要使用基类指针或引用调用子类时,最好将基类的析构函数声明为虚函数,否则可以存在内存泄露的问题。

每个析构函数(不加 virtual) 只负责清除自己的成员。
基类指针可以指向派生类的对象(多态性),如果删除该指针delete []p;就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全,造成内存泄漏。所以,将析构函数声明为虚函数是十分必要的。

反正你在写一个类时,将其析构函数写为虚函数总不会错,一样重载的。

举例说明:
子类B继承自基类A:

A *p = new B; delete p;
  1. 此时,如果类A的析构函数不是虚函数,那么delete p;将会仅仅调用A的析构函数,只释放了B对象中的A部分,而派生出的新的部分未释放掉。
  2. 如果类A的析构函数是虚函数,delete p; 将会先调用B的析构函数,再调用A的析构函数,释放B对象的所有空间。

补充:

B *p = new B; delete p;

此时也是先调用B的析构函数,再调用A的析构函数。

为什么虚函数效率低?

  1. 因为虚函数需要一次间接的寻址,而一般的函数可以在编译时定位到函数的地址,虚函数(动态类型调用)是要根据某个指针定位到函数的地址。多增加了一个过程效率肯定低些,但是带来了运行时的多态。
  2. 跟cpu流水线执行效率有关也是说得通的,究其原因还是因为存在动态跳转,这会导致分支预测失败,流水线排空。

多态子类的调用顺序,为什么不要在构造函数中调用虚函数?

原因是,在子类的构造函数执行时,虚函数表还没有被子类覆盖,换句话说,此时调用的函数是当前类的函数,虚函数机制在构造函数中无法触发。其原因在于子类构造时各个初始化步骤的调用顺序:

  1. 构造子类构造函数的参数
  2. 子类调用基类构造函数
  3. 基类设置vptr
  4. 基类初始化列表内容进行构造
  5. 基类函数体调用
  6. 子类设置vptr
  7. 子类初始化列表内容进行构造
  8. 子类构造函数体调用

注意: 初始化列表内的数据不按参数书写顺序,而是按类内部的定义顺序。 析构的顺序恰好相反,所以也不要在析构函数中调用虚函数,那样也是没有意义的。

4. 重载

运算符重载

https://wuyuans.com/2012/09/cpp-operator-overload/
c++的一大特性就是重载(overload),通过重载可以把功能相似的几个函数合为一个,使得程序更加简洁、高效。在c++中不止函数可以重载,运算符也可以重载。由于一般数据类型间的运算符没有重载的必要,所以运算符重载主要是面向对象之间的。

一般运算符重载:
在进行对象之间的运算时,程序会调用与运算符相对应的函数进行处理,所以运算符重载有两种方式:成员函数和友元函数。成员函数的形式比较简单,就是在类里面定义了一个与操作符相关的函数。友元函数因为没有this指针,所以形参会多一个。

class A
{
	public:
	A(int d) :data(d){}
	A operator+(A&);//成员函数
	A operator-(A&);
	friend A operator+(A&, A&);//友元函数
	friend A operator-(A&, A&);
	private:
	int data;
};
//成员函数的形式
A A::operator+(A &a)
{
	return A(data + a.data);
}
A A::operator-(A &a)
{
	return A(data - a.data);
}
//友元函数的形式
A operator+(A &a1, A &a2)
{
	return A(a1.data + a2.data);
}
A operator-(A &a1, A &a2)
{
	return A(a1.data - a2.data);
}
// 然后我们就可以对类的对象进行+、-、*、/了。
void main(void)
{
	A a1(1), a2(2), a3(3);
	a1 = a2 + a3;
	//或者
	a1 = a2.operator+(a3);
}

注意: 在进行a2+a3的时候会出错,因为我们在上面对+定义了两种方法,去掉一种即可。

重载、重写、重定义的区别

http://www.cnblogs.com/luxiaoxun/archive/2012/08/09/2630751.html
简而言之就是,当子类中有跟基类相同定义的函数时,

  1. 基类(含基类的基类)中该函数声明里如果都没有virtual的话,则是隐藏,生成的子类对象中调用的该函数是子类自己的成员函数而非继承来的基类的同名函数;
  2. 如果基类该函数声明中有virtual,则为改写(override),实现多态:让基类指针p指向子类对象后,通过p调用该函数,其实是调用的子类的该成员函数。

一、重载(overload)
指函数名相同,但是它的参数表列个数或顺序,类型不同。但是不能靠返回类型来判断。
(1)相同的范围(在同一个作用域中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。
(5)返回值可以不同;

二、重写(也称为覆盖 override)
是指派生类重新定义基类的虚函数,特征是:
(1)不在同一个作用域(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有 virtual 关键字,不能有 static。
(5)返回值相同(或是协变),否则报错;<—-协变这个概念我也是第一次才知道…
(6)重写函数的访问修饰符可以不同。尽管virtual是private的,派生类中重写改写为public,protected 也是可以的。

三、重定义(也成隐藏)
(1)不在同一个作用域(分别位于派生类与基类);
(2)函数名字相同;
(3)返回值可以不同;
(4)参数不同。此时,不论有无 virtual 关键字,基类的函数将被隐藏(注意别与重载以及覆盖混淆)。
(5)参数相同,但是基类函数没有 virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

构造和析构

构造函数

  1. 构造函数最重要的作用是创建对象本身。
  2. C++规定,每个类必须有一个构造函数,没有构造函数,就不能创建任何对象。
  3. 如果一个类没有提供任何的构造函数,刚C++提供一个默认的构造函数(由C++编译器提供),这个默认的构造函数的构造函数,它只创建对象,而不做任何的初始化工作。
  4. 只要一个类定义了一个构造函数,不管这个构造函数是否是参数的构造函数,C++就不再提供默认的构造函数,也就是说,如果为一个类定义了一个参数的构造函数,还想要无参数的构造函数,刚必须自己定义。

构造函数的调用顺序

  1. 派生类构造函数中的某些初始化可能是基于基类的,所以规定构造在类层次的最根处开始,而在每一层,首先调用基类构造函数,然后调用成员对象构造函数。因为C++的成员变量是不会自动初始化的。
    注意: 成员函数的构造顺序根据其定义顺序决定,而不是传参顺序。
    析构函数的调用顺序和构造函数相反。
  2. 如果没有显式调用基类的构造函数,会自动调用基类的无参构造函数。而如果基类只有带参数的构造函数,则会报错。
    不一定要显式调用无参构造函数,可以显式调用基类带参数的构造函数。

析构函数

  1. 当一个对象生命周期结束时,其所占有的内在空间就要被 回收,这个工作就由析构函数来完成。
  2. 析构函数是“反向”的构造函数,析构函数不允许有返回值,更重要的是析构函数不允许带参数,并且一个类中只能有一个析构函数。
  3. 析构函数的作用正好与构造函数相反,对象走出其作用范围,对就的内在空间被系统收回或程序用delete删除时,析构函数被调用。
  4. 根据析构函数的这种特点,我们可以在构造函数中初始他对象的成员就是,给其分配内存空间(堆内存),在析构函数中对象运行期间所申请的资源。

析构函数中可以抛出异常吗?

从语法上面讲,析构函数抛出异常是可以的,C++并没有禁止析构函数引发异常,但是C++不推荐这一做法,从析构函数中抛出异常是及其危险的。
析构函数可能在对象正常结束生命周期时调用,也可能在有异常发生时从函数堆栈清理时调用。前一种情况抛出异常不会有无法预料的结果,可以正常捕获;但后一种情况下,因为函数发生了异常而导致函数的局部变量的析构函数被调用,析构函数又抛出异常,本来局部对象抛出的异常应该是由它所在的函数负责捕获的,现在函数既然已经发生了异常,必定不能捕获,因此,异常处理机制只能调用terminate()。若真的不得不从析构函数抛出异常,应该首先检查当前是否有仍未处理的异常,若没有,才可以正常抛出。

析构&构造是否可以重载?

构造函数可以被重载,因为构造函数可以有多个且可以带参数。 析构函数不可以被重载,因为析构函数只能有一个,且不能带参数。

基础语法

数组array和结构体struct的区别?

  1. 定义上的区别

    数组是同类型数据的集合;结构体可以是同类型也可以是不同类型数据的集合。

  2. 调用时候的区别

    数据是直接用形如“数组名[下标]”的方式调用,如a[3],表示数组a的第4个元素(数组下标从0开始);结构体是用结构体成员运算符来调用的,如:std.num,表示调用结构体std中的num变量。

Struct和Class有什么区别?

结构体在C语言中是一种数据结构,结构体不能为空,否则会报错;
换句话说,就是C语言中的结构体只能定义成员变量,但是不能定义成员函数

在C++中和类差不多,但是类的默认为private;结构体的默认为public
然而在C++中既可以定义成员变量又可以定义成员函数,C++中的结构体和类体现了数据结构和算法的结合,是面向对象的概念。

相同之处:
在C++当中,结构体中可以有成员变量,可以有成员函数,也可以定义public、private、protected数据成员,可以从别的类继承,也可以被别的类继承,可以有虚函数。
总的一句话: class和struct的语法基本相同,从声明到使用,都很相似,但是struct的约束要比class多,理论上,struct能做到的class都能做到,但class能做到的stuct却不一定做的到。

两者的区别:
对于成员访问权限以及继承方式,class中默认的是private,而struct中则是public。 class还可以用于表示模板类型,struct则不行。
注意:struct是可以继承与被继承的,这一点有的人可能忽略了。
总结:
1. 概念: class和struct的语法基本相同,从声明到使用,都很相似,但是struct的约束要比class多,理论上,struct能做到的class都能做到,但class能做到的stuct却不一定做的到。
2. 类型: struct是值类型,class是引用类型.
3. 效率: 由于堆栈的执行效率要比堆的执行效率高,但是堆栈资源却很有限,不适合处理逻辑复杂的大对象,因此struct常用来处理作为基类型对待的小对象,而class来处理某个商业逻辑。
4. 关系: struct不仅能继承也能被继承,而且可以实现接口,不过Class可以完全扩展。内部结构有区别,struct只能添加带参的构造函数,不能使用abstract和protected等修饰符,不能初始化实例字段。

如何选择:
(1) 在表示诸如点、矩形等主要用来存储数据的轻量级对象时,首选struct。
(2) 在表示数据量大、逻辑复杂的大对象时,首选class。
(3) 在表现抽象和多级别的对象层次时,class是最佳选择。

指针和引用的区别?

  1. 指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元; 而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。 如:
int a = 1; int *p = &a;
int a = 1; int &b = a;`

上面定义了一个整形变量和一个指针变量p,该指针变量指向a的存储单元,即p的值是a存储单元的地址。 而下面2句定义了一个整形变量a和这个整形a的引用b,事实上a和b是同一个东西,在内存占有同一个存储单元。

  1. 可以有const指针,但是没有const引用;
  2. 指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的)
  3. 指针的值可以为空,但是引用的值不能为NULL,并且引用在定义的时候必须初始化;
  4. 指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了。虽然引用不可以改变指向,但是可以改变初始化对象的内容。
  5. "sizeof引用"得到的是所指向的变量(对象)的大小,而"sizeof指针"得到的是指针本身的大小;
  6. 指针和引用的自增(++)运算意义不一样。
int a = 0;
int b = &a;
int *p = &a;
b++; // 相当于a++; b只是a的一个别名,和a一样使用。
p++; // p指向的下一个内存地址
(*p)++; // 相当于a++

推荐使用引用的好处:

  1. 引用作为函数参数进行传递时,实质上传递的是实参本身,即传递进来的不是实参的一个拷贝,因此对形参的修改其实是对实参的修改,所以在用引用进行参数传递时,不仅节约时间,而且可以节约空间。
  2. 引用比指针使用起来形式上更漂亮。
    使用引用指向的内容时可以直接用引用变量名,而不像指针一样要使用*号;定义引用的时候也不用像指针一样使用&取址。
  3. 引用比指针更安全。 由于不存在空引用,并且引用一旦被初始化为指向一个对象,它就不能被改变为另一个对象的引用,因此引用很安全。
    对于指针来说,它可以随时指向别的对象,并且可以不被初始化,或为NULL,所以不安全。const指针虽然不能改变指向,但仍然存在空指针,并且有可能产生野指针(即多个指针指向一块内存,free掉一个指针之后,别的指针就成了野指针)。

总之,它们的这些差别都可以归结为: 指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名,引用不改变指向。

全局变量

多源文件共用全局变量

多个源文件共用一个全局变量,一般通过extern在头文件.h中声明该变量,然后在源文件.cpp中定义, 如果在头文件中定义的话,多个源文件同时引用该头文件,会造成重复定义的错误。

全局变量和局部变量区别

局部变量可以与全局变量重名,但是局部变量会屏蔽全局变量。要使用全局变量,需要使用“::”。 在函数体内引用变量会用到同名的局部变量而不是全局变量,对于一些编译器来说,在同一个函数体内可以定义多个同名的局部变量。例如我们可以在一个函数内部,在两个循环中都定义同名的局部变量i,而局部变量i的作用域在那个循环体内。
具体区别:

  1. 作用域不同: 全局变量的作用域为整个程序,而局部变量的作用域为当前函数或循环等;
  2. 内存存储方式不同: 全局变量存储在全局数据区中,局部变量存储在栈区;
  3. 生命期不同: 全局变量的生命期和主程序一样,随程序的销毁而销毁,局部变量在函数内部或循环内部,随函数的退出或循环退出就不存在了;
  4. 使用方式不同: 全局变量在声明后程序的各个部分都可以用到,但是局部变量只能在局部使用。函数内部会优先使用局部变量再使用全局变量。需要注意一点的是,局部变量不能赋值为同名全局变量的值。

全局变量、静态全局变量、静态局部变量的区别

  1. 存放位置相同: 全局变量、静态全局变量、静态局部变量都存放在全局/静态存储区中。
  2. 作用域:
  • 全局变量: 具有全局作用域(文件作用域),通常在整个工程中有效,定义在一个文件中,在其他文件中使用extern声明即可使用。
  • 静态全局变量:同样具有全局作用域,但是静态全局变量只能作用在定义该变量的文件中,即使使用extern声明也不行。
  • 静态局部变量:跟局部变量一样,也具有局部作用域,即在定义的函数体外部无法访问。与普通局部变量不同的是,静态局部变量跟全局变量和全局静态变量一样,存储在静态存储区,但只能被初始化一次,初始化完成后直到程序结束一直存在。

静态成员变量

静态成员变量为什么一定要初始化

其实这句话“静态成员变量是需要初始化的”是有一定问题的,应该说“静态成员变量需要定义”才是准确的,而不是初始化。
两者的区别在于:初始化是赋一个初始值,而定义是分配内存
静态成员变量在类中仅仅是声明,没有定义,所以要在类的外面定义,实际上是给静态成员变量分配内存。
可以通过以下几个例子更形象的说明这个问题:

class A {
	public:
	static int a; //声明但未定义
};
int main() {
	printf("%d", A::a);
	return 0;
}

编译以上代码会出现“对‘A::a’未定义的引用”错误。==error: undefined reference to…==这是因为静态成员变量a未定义,也就是还没有分配内存,显然是不可以访问的。
再看如下例子:

class A {
	public:
	static int a; //声明但未定义
};
int A::a = 3; //定义了静态成员变量,同时初始化。也可以写"int A:a;",即不给初值,同样可以通过编译int main() {
printf("%d", A::a);
return 0;
}

这样就对了,因为给a分配了内存,所以可以访问静态成员变量a了。
因为类中的静态成员变量仅仅是声明,暂时不需分配内存,所以我们甚至可以这样写代码:

//a.cppclass B; //这里我们使用前置声明,完全不知道B是什么样子
class A {
public:
static B bb;//声明了一个类型为B的静态成员,在这里编译器并未给bb分配内存。
//因为仅仅是声明bb,所以编译器并不需要知道B是什么样子以及要给其对应的对象分配多大的空间。
//所以使用前置声明"class B"就可以保证编译通过。
};

对于类来说,new一个类对象不仅会分配内存,同时会调用构造函数进行初始化,所以类对象的定义和初始化总是关联在一起。

普通成员函数可以访问静态成员变量吗?

可以,直接[对象.静态成员函数]。
但是静态成员函数不能访问非静态成员变量,如果要访问,必须通过参数传递的方式得到相应的对象,再通过对象来访问。

Const常变量、常指针、常函数

我们都知道在调用成员函数的时候编译器会将对象自身的地址作为隐藏参数传递给函数,在const成员函数中,既不能改变this所指向的对象,也不能改变this所保存的地址,this的类型是一个指向const类型对象的const指针。

怎么区别常指针和常变量

有一个规则可以很好的区分const是修饰指针,还是修饰指针指向的数据——画一条垂直穿过指针声明的星号(*),如果const出现在线的左边,指针指向的数据为常量;如果const出现在右边,指针本身为常量。而引用本身与天俱来就是常量,即不可以改变指向。

Const在函数名前面和后面的区别

当const在函数名前面的时候修饰的是函数返回值,在函数名后面表示是常成员函数,该函数不能修改对象内的任何成员,只能发生读操作,不能发生写操作。

模板声明格式

函数模板的格式:

template <class 形参名,class 形参名,......> 返回类型 函数名(参数列表)
{
函数体
}

类模板的格式为:

template<class 形参名 ,class 形参名,…> class 类名
{ ... };

C和C++内存模型

详解:http://chenqx.github.io/2014/09/25/Cpp-Memory-Management/
http://blog.csdn.net/qq_33266987/article/details/51965221
一个由C/C++编译的程序占用的内存分为以下几个部分:
在这里插入图片描述

  1. 栈区(stack)
    由编译器自动分配、释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈,如果还不清楚,那么就把它想成数组,它的内存分配是连续分配的,即,所分配的内存是在一块连续的内存区域内.当我们声明变量时,那么编译器会自动接着当前栈区的结尾来分配内存。 栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  2. 堆区(heap)
    存放new的对象。一般由程序员分配释放,一般一个new就要对应一个 delete, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式类似于链表,在内存中的分布不是连续的,它们是不同区域的内存块通过指针链接起来的.一旦某一节点从链中断开,我们要人为的把所断开的节点从内存中释放。
    涉及的问题:“缓冲区溢出”、“内存泄露”。
  3. 全局区(静态区)(static)
    全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,程序结束后有系统释放。在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
  4. 文字常量区
    他们里面存放的是常量,不允许修改。常量字符串就是放在这里的。 程序结束后由系统释放。
  5. 程序代码区
    存放函数体的二进制代码。

C++中怎么为一个函数分配内存?

内存分配方式有几种?

  1. 从静态存储区域分配内存
    程序编译的时候内存已经分配好了,并且在程序的整个运行期间都存在,例如全局变量。
  2. 在栈上创建
    在执行函数时,函数内局部变量的存储单元可以在栈上创建,函数结束时这些存储单元自动被释放。 处理器的指定集中有关于栈内存的分配运算,因此效率比较高,但是分配的内存容量有限。
  3. 在堆上分配内存
    亦称动态内存分配,程序在运行的时候用malloc函数或new运算符申请任意大小的内存,程序员要用free函数或delete运算符释放内存。动态内存使用非常灵活,但问题也很多。

malloc() 和 new

http://blog.jobbole.com/102002/
http://blog.csdn.net/zjc156m/article/details/16819357

malloc()

  1. malloc的全称是memory allocation,中文叫动态内存分配。
    原型: extern void *malloc(unsigned int num_bytes);
    说明: 分配长度为num_bytes字节的内存块。如果分配成功则返回指向被分配内存的指针,分配失败返回空指针NULL。当内存不再使用时,应使用free()函数将内存块释放。
  2. void malloc(int size);
    说明: malloc向系统申请分配指定size个字节的内存空间,返回类型是 void
    类型。void* 表示未确定类型的指针。C,C++规定,void* 类型可以强制转换为任何其它类型的指针。
    备注: void* 表示未确定类型的指针,更明确的说是指申请内存空间时还不知道用户是用这段空间来存储什么类型的数据(比如是char还是int或者…)
  3. free void free(void *FirstByte)
    说明: 该函数是将之前用malloc分配的空间还给程序或者是操作系统,也就是释放了这块内存,让它重新得到自由。
  4. 注意事项
  • 申请了内存空间后,必须检查是否分配成功。
  • 当不需要再使用申请的内存时,记得释放;释放后应该把指向这块内存的指针指向NULL,防止程序后面不小心使用了它。
  • 这两个函数应该是配对。如果申请后不释放就是内存泄露;如果无故释放那就是什么也没有做。释放只能一次,如果释放两次及两次以上会出现错误(释放空指针例外,释放空指针其实也等于啥也没做,所以释放空指针释放多少次都没有问题)。
  • 虽然malloc()函数的类型是(void *),任何类型的指针都可以转换成(void *),但是最好还是在前面进行强制类型转换,因为这样可以躲过一些编译器的检查。
  • malloc()到底从哪里得到了内存空间?
    答案是从堆里面获得空间。也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。

new

  1. C++中,用new和delete动态创建和释放数组或单个对象。
    动态创建对象时,只需指定其数据类型,而不必为该对象命名,new表达式返回指向该新创建对象的指针,我们可以通过指针来访问此对象。
    下面这个new表达式在堆区中分配创建了一个整型对象,并返回此对象的地址,并用该地址初始化指针pi 。

    int *pi = new int;
    
  2. 动态创建对象的初始化 动态创建的对象可以用初始化变量的方式初始化。

    int *pi = new int(100); //指针pi所指向的对象初始化为100string *ps=new string(10,’9’);//*ps 为“9999999999”
    
  3. 撤销动态创建的对象delete表达式释放指针指向的地址空间。如果指针指向的不是new分配的内存地址,则使用delete是不合法的。

    delete pi ;// 释放单个对象delete [ ]pi;//释放数组
    
  4. 在delete之后,重设指针的值
    delete p; // 执行完该语句后,p变成了不确定的指针,在很多机器上,尽管p值没有明确定义,但仍然存放了它之前所指对象的地址,然后p所指向的内存已经被释放了,所以p不再有效。此时,该指针变成了悬垂指针(悬垂指针指向曾经存放对象的内存,但该对象已经不存在了)。悬垂指针往往导致程序错误,而且很难检测出来。 一旦删除了指针所指的对象,立即将指针置为0,这样就非常清楚的指明指针不再指向任何对象。(零值指针:int *ip = 0;)

  5. 区分零值指针和NULL指针
    零值指针,是值是0的指针,可以是任何一种指针类型,可以是通用变体类型void也可以是char,int*等等。
    空指针,其实空指针只是一种编程概念,就如一个容器可能有空和非空两种基本状态,而在非空时可能里面存储了一个数值是0,因此空指针是人为认为的指针不提供任何地址讯息。参考:http://www.cnblogs.com/fly1988happy/archive/2012/04/16/2452021.html

  6. new分配失败时,返回什么?
    1993年前,C++一直要求在内存分配失败时operator new要返回0,现在则是要求operator new抛出std::bad_alloc异常。很多C++程序是在编译器开始支持新规范前写的。C++标准委员会不想放弃那些已有的遵循返回0规范的代码,所以他们提供了另外形式的operator new(以及operator new[])以继续提供返回0功能。这些形式被称为“无抛出”,因为他们没用过一个throw,而是在使用new的入口点采用了nothrow对象:

    class widget { ... };
    widget *pw1 = new widget;// 分配失败抛出std::bad_alloc if(pw1 == 0) ... // 这个检查一定失败
    widget *pw2 = new (nothrow) widget; // 若分配失败返回0if (pw2 == 0) ... // 这个检查可能会成功
    

malloc()和new的区别

  1. new 返回指定类型的指针,并且可以自动计算所需要大小。 比如:

    int *p;   p = new int;
    

    返回类型为int* 类型(整数型指针),分配大小为 sizeof(int); 而malloc则必须要由我们计算字节数,并且在返回后强行转换为实际类型的指针。

    	int *p;   
    	p = (int *) malloc (sizeof(int)*128);
    

    分配128个(可根据实际需要替换该数值)整型存储单元,并将这128个连续的整型存储单元的首地址存储到指针变量p中

    double *pd=(double *) malloc (sizeof(double)*12);
    

分配12个double型存储单元,并将首地址存储到指针变量pd中。

  1. malloc只管分配内存,并不能对所得的内存进行初始化,所以得到的一片新内存中,其值将是随机的。
    除了分配及最后释放的方法不一样以外,通过malloc或new得到指针,在其它操作上保持一致。

有了malloc/free为什么还要new/delete?

  1. malloc与free是C++/C语言的标准库函数new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
  2. 对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free
    因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。
    我们不要企图用malloc/free来完成动态对象的内存管理,应该用new/delete。由于内部数据类型的“对象”没有构造与析构的过程,对它们而言malloc/free和new/delete是等价的。
  3. 既然new/delete的功能完全覆盖了malloc/free,为什么C++不把malloc/free淘汰出局呢?
    这是因为C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存
    如果用free释放“new创建的动态对象”,那么该对象因无法执行析构函数而可能导致程序出错。如果用delete释放“malloc申请的动态内存”,结果也会导致程序出错,但是该程序的可读性很差。所以new/delete必须配对使用,malloc/free也一样。
    在这里插入图片描述

拷贝构造函数(浅拷贝、深拷贝)

拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它必须的一个参数是本类型的一个引用变量。
如:CExample(const CExample& C)
在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝。

深拷贝和浅拷贝可以简单理解为:
如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。
在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。

拷贝构造函数出现场景:
当用一个已初始化过了的自定义类类型对象去初始化另一个新构造的对象的时候,拷贝构造函数就会被自动调用。以下情况都会调用拷贝构造函数:
(1)一个对象以值传递的方式传入函数体
(2)一个对象以值传递的方式从函数返回
(3)一个对象需要通过另外一个对象进行初始化。

什么情况下必须定义拷贝构造函数?
当类的对象用于函数值传递时(值参数,返回类对象),拷贝构造函数会被调用。如果对象复制并非简单的值拷贝,那就必须定义拷贝构造函数。例如大的堆栈数据拷贝。如果定义了拷贝构造函数,那也必须重载赋值操作符。

大多情况下“浅拷贝”已经能很好地工作了,但是一旦对象存在了动态成员,那么浅拷贝就会出问题了。因为默认的拷贝构造函数是按成员拷贝构造,这导致了两个不同的指针(如ptr1=ptr2)指向了相同的内存。当一个实例销毁时,调用析构函数 free(ptr1)释放了这段内存,那么剩下的一个实例的指针ptr2就无效了,在被销毁的时候free(ptr2)就会出现错误了, 这相当于重复释放一块内存两次。这种情况必须显式声明并实现自己的拷贝构造函数,来为新的实例的指针分配新的内存。 简而言之,当数据成员中有指针时,必须要用深拷贝。

自定义拷贝构造函数是一种良好的编程风格,它可以阻止编译器形成默认的拷贝构造函数,提高源码效率。

以下函数哪个是拷贝构造函数,为什么?

X::X(const X&); //拷贝构造函数
X::X(X);
X::X(X&, int a=1); //拷贝构造函数
X::X(X&, int a=1, int b=2); //拷贝构造函数

解答:对于一个类X, 如果一个构造函数的第一个参数是下列之一: a) X&
b) const X&
c) volatile X&
d) const volatile X&
且没有其他参数或其他参数都有默认值,那么这个函数是拷贝构造函数。

一个类中可以存在多于一个的拷贝构造函数吗?
解答:可以。

class X {
public:
X(const X&); // const 的拷贝构造
X(X&); // 非const的拷贝构造
};

**注意: **如果一个类中只存在一个参数为 X& 的拷贝构造函数,那么就不能使用const X或volatile X的对象实行拷贝初始化。

如果一个类中没有定义拷贝构造函数,那么编译器会自动产生一个默认的拷贝构造函数。这个默认的参数可能为 X::X(const X&)或 X::X(X&),由编译器根据上下文决定选择哪一个。

内存拷贝函数memcpy

原型: void *memcpy(void *dest, const void *src, unsigned int count);
功能: 由src所指内存区域复制count个字节到dest所指内存区域。
说明: src和dest所指内存区域不能重叠,函数返回指向dest的指针。

void * MyMemMove(void *dst,const void *src,int count)
{
	assert(dst);
	assert(src);
	void * ret = dst;
	if (dst <= src || (char *)dst >= ((char *)src + count)) 
	{
		while (count--) 
		{
			*(char *)dst = *(char *)src;
			dst = (char *)dst + 1;
			src = (char *)src + 1;
		}
	}
	else 
	{
		dst = (char *)dst + count - 1;
		src = (char *)src + count - 1;
		while (count--) 
		{
			*(char *)dst = *(char *)src;
			dst = (char *)dst - 1;
			src = (char *)src - 1;
		}
	}
	return(ret);
}

strcpy()实现

char * strcpy(char *dst,const char *src) //[1]
{
	assert(dst != NULL && src != NULL); //[2]
	char *ret = dst; //[3]
	while ((*dst++=*src++)!='\0'); //[4]
	return ret;
}

C++中32位单精度浮点数有效数字是多少位?

浮点数在计算机中存储时,按照二进制科学计数法拆分为三个部分:符号位、指数部分和尾数部分。
在这里插入图片描述
单精度浮点数(float)总共用32位来表示浮点数,其中尾数用23位存储,加上小数点前有一位隐藏的1(IEEE754规约数表示法),
2^(23+1) = 16777216。因为 10^7 < 16777216 < 10^8

所以说单精度浮点数的有效位数是7位。考虑到第7位可能的四舍五入问题,所以单精度最少有6位有效数字(最小尺寸)。

同样地:双精度浮点数(double)总共用64位来表示浮点数,其中尾数用52位存储,
2^(52+1) = 9007199254740992,10^16 < 9007199254740992 < 10^17

所以双精度的有效位数是16位。同样四舍五入,最少15位。

数据结构的对齐方式

  1. 在 64 位 Linux 下,结构体字段默认按 8 字节对齐;32 位 Linux 下,默认 4 字节对齐。
  2. 显示指定对齐方式时,会受到机器字长的约束,即 64 位 Linux 下可以按 8 字节及以下的任意字节对齐,32 位只能按 4
    字节及以下任意字节对齐。

对于标准数据类型,它的地址只要是它的长度的整数倍就行了,而非标准数据类型按下面的原则对齐:
数组: 按照基本数据类型对齐,第一个对齐了后面的自然也就对齐了。
联合: 按其包含的长度最大的数据类型对齐。
结构体: 结构体中每个数据类型都要对齐。

什么是字节对齐?

在C语言中,结构是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如数组、结构、联合等)的数据单元。在结构中,编译器为结构的每个成员按其自然边界(alignment)分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。 为了使CPU能够对变量进行快速的访问,变量的起始地址应该具有某些特性,即所谓的”对齐”。 比如4字节的int型,其起始地址应该位于4字节的边界上,即起始地址能够被4整除。

字节对齐有什么作用?

字节对齐的作用不仅是便于cpu快速访问,同时合理的利用字节对齐可以有效地节省存储空间。

对于32位机来说,4字节对齐能够使cpu访问速度提高,比如说一个long类型的变量,如果跨越了4字节边界存储,那么cpu要读取两次,这样效率就低了。但是在32位机中使用1字节或者2字节对齐,反而会使变量访问速度降低。所以这要考虑处理器类型,另外还得考虑编译器的类型。在vc中默认是4字节对齐的,GNU gcc 也是默认4字节对齐。

Linux相关

linux系统的一些基本命令

http://www.cnblogs.com/laov/p/3541414.html

编译调试

头文件引用方式

#include头文件有两种方式,一种是尖括号如<filename.h>、一种是双引号如“filename.h”,不同引用方式的查找路径也不一样。
<>的查找路径顺序为:
1、“-I dir1 –I dir2 …”编译选项指定的路径目录;
2、标准路径,即系统或用户配置的路径,如/usr/include,/usr/local/include等;
3、不会在当前目录下寻找头文件;
“”的查找路径顺序为: 1、当前目录;
2、“-I dir1 –I dir2 …”编译选项指定的路径目录;
3、标准路径

如何调试出内存泄露的问题

http://blog.csdn.net/sunnylion1982/article/details/8186801/
内存泄漏是编程中常常见到的一个问题,内存泄漏往往会一种奇怪的方式来表现出来,基本上每个程序都表现出不同的方式。 但是一般最后的结果只有两个,一个是程序当掉,一个是系统内存不足。 还有一种就是比较介于中间的结果程序不会当,但是系统的反映时间明显降低,需要定时的Reboot才会正常。 有一个很简单的办法来检查一个程序是否有内存泄漏。就是是用Windows的任务管理器(Task Manager)。运行程序,然后在任务管理器里面查看 “内存使用”和”虚拟内存大小”两项,当程序请求了它所需要的内存之后,如果虚拟内存还是持续的增长的话,就说明了这个程序有内存泄漏问题。 当然如果内存泄漏的数目非常的小,用这种方法可能要过很长时间才能看的出来。 当然最简单的办法大概就是用CompuWare的BoundChecker 之类的工具来检测了,不过这些工具的价格对于个人来讲稍微有点奢侈了。 如果是已经发布的程序,检查是否有内存泄漏是又费时又费力。所以内存泄漏应该在Code的生成过程就要时刻进行检查。

内存泄漏产生的原因一般是三种情况:

  1. 分配完内存之后忘了回收;
  2. 程序Code有问题,造成没有办法回收;
  3. 某些API函数操作不正确,造成内存泄漏。

程序崩溃的原因和确定问题的方法:

  1. 函数栈溢出 一个变量未初化、未赋值,就读取它的值。 ( 这属于逻辑问题,往往是粗心大意的导致的 )
  2. 函数栈溢出 (1)定义了一个体积太大的局部变量 (2)函数嵌套调用,层次过深(如无穷递归)
  3. 数组越界访问 访问数组元素时,下标越界
  4. 指针的目标对象不可用 (1)空指针 (2)野指针 ○ 指针未赋值 ○ free/delete释放了的对象 ○ 不恰当的指针强制转换
  5. 内存泄漏?
发布了9 篇原创文章 · 获赞 11 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/k_atherine/article/details/101215329