C++自学总结(持续更新ing)

C++的内存管理:


与Java类似,C++把函数、局部变量放在栈空间,把对象放在了堆空间。当一个类为空类,但是创建了空类的对象,编译器同样会在堆空间分类内存。如果使用的Visual Studio的编译器,那么按照规定,每个类至少分配1Byte的空间。所以sizeof(一个空类的对象),将会得到结果1。注意sizeof求的是有多少个byte.


如果在类当中包含了一个构造函数和析构函数,其对象的size同样只是1.调用构造函数和析构函数只需要函数地址,而这些函数的地址只与类型有关(放在了栈空间),与类型的实例无关(不放在堆空间)。所以sizeof(这个类的对象)同样只是1.


但是如果析构函数标注为虚函数的话,编译如果发现某个类包含虚函数,就是为这个类型生成虚函数表,为这个类型的每一个实例(在堆内存)中添加一个指向虚函数表的指针。


C++的指针的size:


在32位的机子上就是32位(4bytes),64位的机子则是64位(8bytes)。


C++字符数组的size: 


char a1[] = "abc";

sizeof( a1 ); // 结果为4,字符 末尾还存在一个NULL终止符 


C++的作用域【1】:


::是运算符中等级最高的,它分为三种:
1)global scope(全局作用域符),用法(::name)
2)class scope(类作用域符),用法(class::name)

这种用法是把成员函数在类外定义的时候用的,譬如:

void Date:: setDate(int y, int m, int d){}进行在类外进行内容定义。
3)namespace scope(命名空间作用域符),用法(namespace::name)
他们都是左关联(left-associativity)
他们的作用都是为了更明确的调用你想要的变量,如在程序中的某一处你想调用全局变量a,那么就写成::a,如果想调用class A中的成员变量a,那么就写成A::a,另外一个如果想调用namespace std中的cout成员,你就写成std::cout(相当于using namespace
std;cout)意思是在这里我想用cout对象是命名空间std中的cout(即就是标准库里边的cout)


所以,可以得出:std::cout <==> using name space std; cout; (可以在代码的中间临时转换命名空间)


C++的核心——指针:


1)函数形参的类型问题:指针(地址传递) 还是 正常的值传递


如Java一样,C++的函数参数的传递同样分为值传递和地址传递。值传递的话,编译器会在函数运行的时候,取该参数的值,复制到自己的同名局部变量上。在该局部变量上的任何修改,都不会影响到放到参数列表上的变量。而当为地址传递则相反,直接修改参数列表上的变量。所以我们需要注意的是,值传递的机制决定了如果实参是一个自己定义的类的实例对象时,就会自动调用复制构造函数。因为值传递的实质就是 '形参' = '实参',这个赋值符号一旦出现在自定义的实例对象之间时,就会调用复制构造函数。这一点是不同于Java的。由于Java的对象全部都是通常都是通过new来创建,所以创建的实例对象的符号其实是一个指针,指向的是实例对象的堆地址。所以在Java当中,当出现实例对象之间的赋值时,传递的是指针的地址,并没有重新创建一个新的对象:


如下面的代码


class test1{
	public test1(int input){
		this.temp = input;
	}
	
	public int temp;
}

public class try1 {
	
	public static void testFunc(test1 input, int change){
		input.temp = change;
	}
	
	public static void main(String[] args) {
		test1 object1 = new test1 (1);
		try1.testFunc(object1, 2);
		System.out.println(object1.temp);
	}

}

object1的temp属性在经过testFunc之后,就会变成2了。因为Java并没有默认的赋值构造函数,除非用户自定义。造成上述的现象的根本原因是,java的对象名实质是一个指针,指向实例对象在堆内存的首地址。所以,作为函数实参时,调用了java函数的地址传递机制。


所以,在C++中,是不允许复制构造函数的参数是实例对象的,只能是实例对象的指针,否则一旦调用复制构造函数,就会导致栈溢出。


说到复制构造函数,不得不提的是会自觉调用复制构造函数的例子:

① 显示地调用,进行实例对象的成员数据赋值

② 函数的形参是某个类的实例对象

③ 函数的返回值是某个类的实例对象


这里需要注意的一点是,默认的复制构造函数的方式都是“浅复制”,意味着,当我们只是调用默认版本的复制构造函数时,如果实例对象当中含有成员数据是一个指针的话,系统会自动复制地址到复制的实例对象成员指针中。所以造成两个对象的数据指针成员指向同一个地址的情况。而这种情况都是我们需要避免的,我们必须定义自己的复制构造函数,然后进行”深复制“。 


2)用指针去访问一个实例对象的成员:


这属于C++与Java的不同:实例对象的属性有利用类的指针去访问的机制:首先使用类的指针指向一个实例对象,然后直接使用该指针访问该实例对象的属性。

在涉及继承的范畴里,这里需要提到的是我们可以用基类指针指向基类对象,派生类指针指向派生类对象,基类指针指向派生类对象,派生类指针指向基类对象。

无疑前两种都是理所当然的。涉及到后两种情况的时候,我们有时候就需要类型转换:

当基类指针想调用只有派生类的成员的时候,必须进行类型转换,同时,派生类指针必须进行类型转换,才能指向基类对象。(注意是才能指向,不是才能访问)


3)用基类指针去访问子类的虚函数——多态(使用基类指针调用派生类的不同实现版本)

(待补充)


C++的面向对象——类:


1)与Java不同的一点是,在C++里面没有包的概念。所以没有默认权限(包内可见)的选项。缺省权限限制则默认为private.

2)与Java不同的另外一点是,C++的类可以用struct 来定义,但是,如果是struct定义的时候,缺省的权限默认为public.

3)与Java不同的还有一点,C++的对象成员可以使用实例对象的指针去调用(使用 ->),除了直接使用实例对象调用之外(.)

4) 与Java不同的还有一点是,C++的创建对象有两种方式,一种是直接调用构造函数如 Date dateObjectName; 如果是有参数的构造函数,则为Date dateObjectName(xx,xx,xx). 另外一种则是通过new获得创建的实例对象的堆地址。方式是Date * pd = new Date。后者机制和Java是一样的,new 返回的是对象的堆内首地址。但是两者有一点最大的不同是,如果使用前者创建对象,则在创建对象的函数运行结束后(出栈),系统会自动调用该类的析构函数进行内存释放。但是对于后者而言,必须通过显式的delete(pd)去释放内存,否则造成内存泄漏。但是如果我知道一个对象名,需要把它转换成为指针的话,需要用到取地址符 &(对象名)然后赋值给一个新建的对象指针。

5)与Java还有一点很重要的不同是:C++的类成员函数的声明和定义默认是需要分开的,在类定义的时候声明,在类外定义(通过作用域符 class :: func name)。简单的成员函数的定义可以在类的定义中实现,编译器会作内联函数处理。关于内联函数,其实就是编译器在调用这种函数的时候,尝试着直接调用其中的语句,称为函数展开,而省去了保存现场,变元要进栈,各种寄存器内容要保存;函数返回时,又要恢复他们的内容的这种开销。inline对编译器是一种请求,而不是命令。编译器可以选择忽略它。还有,一些编译器不能内联所有类型的函数。例如,通常编译器不能内联递归函数。必须查阅自己的编译器用户手册以了解对内联的限制。如果一个函数不能被内联,它就被当作一个正常的函数调用。【3】

6)  与Java还有一点非常不同的地方是:对于静态的数据成员(注意不是函数成员),必须类中声明,类外定义。(待补充)

7)  与Java不同的还有:C++的类可以定义一种特殊的类成员,叫友元。(待补充)


C++类的三种特殊成员:


常成员:常成员细分为三类:常数据成员,常对象,常成员函数。


对于常数据成员,C++规定必须使用构造函数进行初始化。初始化的方式有两种:

① Mclass():M(5),对Mclass里面的 const int M 进行赋值;

② 使用带参数的构造函数:


常对象:常对象就是全部的数据成员都是常成员,在通过该类的构造函数初始化后,不能再通过指针或者对象调用修改。


常成员函数:对于成员函数的调用过程如下所述,编译器会自动地给成员函数的this指针赋值。this指针本身就是一个常指针(指针指向的地址赋值后就不能再修改)。当一个函数是常成员函数的时候,编译器便会认为这个this 这个常指针是指向一个常对象的。所以,常成员函数不能修改指向的对象的一切成员数据。


静态成员


C++的普通类成员函数调用过程【2】:


c++的成员函数根据其调用的不同,大致可以分为4类:内联成员函数,静态成员函数,虚成员函数和上述3种以外的普通成员函数。从本质来说类成员函数和全局函数在调用上并没有差别,非内联函数的在调用时,基本上都包括如下的过程:函数的参数入栈,eip指针值入栈,然后跳到函数体的地址,执行函数体对应的代码,执行完毕调整栈帧。


这里我们先分析下普通的成员函数,


普通的成员函数在被调用时有两大特征:


1 普通的成员函数是静态绑定的, 
2 普通的成员函数调用时编译器隐式传入this指针的值。


什么叫静态绑定呢?这个是为了和虚函数作为对比的。虚函数为了实现动态联编,源码在编译的时候,编译器不是翻译成直接调用Test类中Print()的汇编代码,而是翻译成一个查找虚表,得到到函数的相对地址的过程。


静态绑定实质是c++源代码编译时,编编译器在p->Print();处翻译成直接调用Test类中Print()的汇编代码,也就是编译期编译器就确定了被调函数的相对地址。其实C++函数调用的本质就是,编译器编译到函数调用时,确定该函数体(函数代码)的栈存储地址,然后就知道在这里需要执行什么代码了。所以类的成员函数和堆空间无关,和栈空间有关。


编译器调用Print()时是根据p类型来确定调用哪个类的Print()函数时,也就是说根据->(或者.)左边对象的类型来确定调用的函数,同时编译器也是根据对象的类型来确定该成员函数是否能够被合法的调用,而这个校验是发生在编译期的类型静态检查的,也就是只是一个代码级的检查的。不管对象的真正类型是什么,只要被强制转化成了Test类型,编译器就会接受p->Print(2);的调用,从而翻译成调用Print的代码。


函数参数入栈后,this指针的值也会入栈或者存入ecx寄存器。而this指针的值可以认为是p的值,也就是->左边对象的值。传入this值的目的是为了操作对象里的数据,通过类的声明,编译器可以确定对象内成员变量的相对于类对象起始地址的偏移,即相对this值的偏移。而成员函数调用时隐式传入的this值,编译器是不对this值进行检查,编译器只是简单生成this+偏移操作对象的汇编代码,所以->左边对象的类型正确,编译器就会找到相应的成员函数,不管传入this值是否正确,只要this+偏移访问的地址是合法的,os也不会抱怨,一旦this+偏移不合法,激活os的异常机制,程序才会宕了。


Reference:

[1] http://www.xuebuyuan.com/1913013.html

[2] http://blog.csdn.net/Demon__Hunter/article/details/5397906

[3] http://zhidao.baidu.com/question/1731639301750140667.html

你可能感兴趣的:(C++)