深度剖析C++第一部分
1、类是一种模型,这种模型可以创建出一个对应的实体。有了类不一定有对应的实体,但是一个实体必定属于某一个类。
2、类用于抽象的描述 一类事物所持有的属性和行为;对象是具体的事物,拥有所属类中描述的一切 属性和行为。
3、类一定都源于生活,两个对象实例不可能完全相同。
4、类之间的基本关系:继承和组合。
继承:从已有的父类细分出来的类和原有的类之间具有继承关系(is-a);继承的子类拥有父类的所有属性和方法。
组合:一些类的存在必须依赖于其他类,组合的类在某一个局部上由其他的类 组成 ;组合关系是整体和局部的关系(生死共存亡,比如电脑类由CPU类、内存类的、显卡类等组成)
5、一个类通常分为以下两个部分:类的实现细节(类的实现)和类的使用方式(类的声明);c++中的类支持声明和实现的分离;将类的实现和定义分开。.h头文件只有成员变量和成员函数的声明,.cpp文件中完成类的实现。
必须在类的表示方法中定义属性和行为的公开级别,类似文件系统中文件的权限。需要注意的一点是访问权限public和private都是针对类外部的访问而言的,在类内部无论是public还是private都是可以访问成员变量和成员函数的,即类成员的作用域与访问级别没有关系。
6、c++中struct和class的主要区别就是成员变量和成员函数的默认访问级别不同,struct的默认访问级别是public的,而class是private的。
7、从程序设计的角度来看,对象也只是个变量,他的类型是类 类型,因此在栈上创建对象时,成员变量初始值为随机值(eg:Test t);在堆上创建 对象时,成员变量初始值为随机值(eg:Test t =Test() 或者是Test *t =new Test(),注意这两种的区别,前者是一个临时对象的拷贝(构造函数本质上就是一个临时对象),后者是类对象指针的定义);在静态存储区创建对象时成员变量初始值为0(eg:全局变量)。
8、构造函数在对象创建后给对象的状态进行初始化,而初始化列表先于构造函数执行,是在对象创建时对对象进行初始化。
9、构造函数没有返回值,但可以有参数,即可以重载;而析构函数没有返回值也没有参数。
经典代码示例:
- #include <stdio.h>
- #include <iostream>
- class Test
- {
- private:
- int i;
- int j;
- public:
- Test()
- {
- i=0;
- j=0;
- }
- Test(int v1,int v2)
- {
- i=v1;
- j=v2;
- }
- ~Test()
- {
- printf("我是析构函数 ");
- }
- void sleep();
- void setValue(int m,int n);
- int getValue_i();
- int getValue_j();
- void eat();
- };
- void Test::sleep()
- {
- printf("我是睡觉动作 ");
- }
- void Test::eat()
- {
- printf("我是吃饭动作 ");
- }
- void Test::setValue(int m,int n)
- {
- i=m;
- j=n;
- }
- int Test::getValue_i()
- {
- return i;
- }
- int Test::getValue_j()
- {
- return j;
- }
- int main()
- {
- // ①Test t=Test(1,2);
- // ②Test t;
- // ③Test t(1,2);
- Test *t=new Test();//④
- t->setValue(1,2);
- printf("i=%d ",t->getValue_i());
- printf("j=%d ",t->getValue_j());
- t->~Test();//调用析构函数
- system("pause");
- return 0;
- }
10、对于构造函数,一个类中可以存在多个重载的构造函数(参数及其类型可以不同),构造函数的重载遵循C++重载的规则。注意:对象的定义和对象的声明不一样,对象的定义是指申请一个对应对象的空间并且调用构造函数;而对象声明是告诉编译器存在这样一个对象 。对象定义时即会触发 构造函数的调用,在一些情况下可以手动调用构造函数。
11、两个特殊的构造函数:(1)无参构造函数(2)拷贝构造函数(该种构造函数在创建对象时拷贝对象的状态)
(1)无参构造函数就是没有参数的构造函数,函数体为空 Test(){}
(2)拷贝构造函数就是参数为(const class_name &obj)的构造函数;当类中没有定义拷贝构造函数时,编译器会默认提供一个拷贝构造函数,简单的进行成员变量的值复制(浅拷贝) 。 【 编译器提供的就是浅拷贝。】
拷贝函数的意义:兼容C语言的初始化方式 ,另外初始化行为能够符合预期的逻辑。
拷贝构造函数包含浅拷贝(拷贝后对象的物理状态相同)和深拷贝(拷贝后对象的逻辑状态相同)两种;有时候在资源分配上浅拷贝会带来一些问题,比如在类中我随便定义一个指针,这个指针指向的内存地址为0x1234;当使用浅拷贝时候(我们把一个对象复制给另一个对象),拷贝后的那个对象中的指针也同样会指向内存地址0x1234;那么我们在释放指针空间的时候只能释放一次,
这就是浅拷贝物理意义上的拷贝;而 如果是深拷贝的话我们在把一个对象的指针复制给另一个对象的时候,会在内存空间重新找一块区域,把数值放进去,在释放指针的时候也可以两次释放,这就是逻辑状态下的深拷贝。
(3)什么时候需要深拷贝?一般情况下对象中有成员指代了系统中的资源(包括动态内存空间、指针定义、文件打开、网络端口的使用等等)时候需要深拷贝,当我们在类中自定义了拷贝构造函数时,必然要实现深拷贝(否则就使用编译器提供的浅拷贝了)。
代码示例:
自定义浅拷贝构造函数
- #include <stdio.h>
- #include <iostream>
- class Test
- {
- private:
- int i;
- int j;
- public:
- Test()
- {
- i=1;
- j=2;
- }
- Test(const Test &obj)
- {
- i=obj.i;
- j=obj.j;
- printf("我是显示调用的构造函数 ");
- }
- int getValue()
- {
- return i+j;
- }
- };
- int main()
- {
- Test t1;
- Test t2(t1);//Test t2=t1;兼容C语言的复制
- printf("value =%d ",t2.getValue());
- system("pause");
- return 0;
- }
自定义深度拷贝构造函数
- #include <stdio.h>
- #include <iostream>
- /*
- #include "DataStruct.h"
- #include <array>
- */
- class Test
- {
- private:
- int i;
- int j;
- public:
- int *p;
- public:
- Test()
- {
- i=1;
- j=2;
- p=new int; //重要
- *p=10;
- }
- Test(const Test &obj)
- {
- i=obj.i;
- j=obj.j;
- p=new int; //重要
- *p=*obj.p; //逻辑拷贝 只把值拷贝过来 p的地址可以与t1的不同
- printf("拷贝后p的地址为:%p ",p);
- printf("我是显示调用的构造函数 ");
- }
- int getValue()
- {
- return i+j;
- }
- void free()
- {
- delete(p);
- }
- };
- int main()
- {
- Test t1;
- Test t2(t1);//Test t2=t1;兼容C语言的复制
- printf("t1.p=%p ",t1.p);
- printf("t2.p=%p ",t2.p);
- printf("value =%d ",t2.getValue());
- t1.free(); //深拷贝时实现两侧free释放
- t2.free();
- system("pause");
- return 0;
- }
12、类中是可以定义const成员的,c++中提供了初始化列表对成员变量进行初始化,语法规则:
ClassName::ClassName():m1(v1),m2(v2),m3(v3){
//其他的一些初始化操作
}
类中的const成员是会被分配空间的,它的本质是只读变量,且类中的const成员只能在初始化列表中指定初始值。编译器无法直接得到const成员的初始值,因此无法进入符号表成为真正意义上的常量。const成员变量为只读变量。const成员变量必须 在初始化列表中指定初值。初始化列表先于构造函数体执行。类中可以使用初始化列表对成员进行初始化。初始化列表的初始化顺序与成员变量的声明顺序有关,与初始化列表的顺序位置无关。
初始化列表代码示例:
- #include <stdio.h>
- class Value
- {
- private:
- int mi;
- public:
- Value(int i)
- {
- printf("i = %d ", i);
- mi = i;
- }
- int getI()
- {
- return mi;
- }
- };
- class Test
- {
- private:
- Value m2;
- Value m3;
- Value m1;
- public:
- Test() : m1(1), m2(2), m3(3)
- {
- printf("Test::Test() ");
- }
- };
- int main()
- {
- Test t;
- return 0;
- }
13、类中多个对象的构造顺序:对于局部对象,当程序执行流到达对象的定义语句时进行构造。对于堆对象,当程序流到达new语句时创建对象,使用new创建对象将自动触发构造函数的调用;对于全局对象,对象的构造顺序是不确定的,不同的编译器使用不同的规则来确定构造顺序。
14、析构函数的定义准则:当类中自定义了构造函数,且构造函数中使用了系统资源(指针、内存操作、文件等)则需要自定义析构函数;析构函数没有返回值也没有参数。析构函数是对象销毁时进行清理的特殊函数,析构函数在对象销毁时被自动调用,他是对象释放系统资源的保障。
15、直接调用构造函数会产生一个临时对象,临时对象的生命周期只有执行一条语句的时间,临时对象的作用域只在一条语句中。实际开发中要人为的避开临时对象。
16、一般情况下都需要显式自定义构造函数和析构函数 ,总结:构造函数必定义;析构函数必定义。拷贝构造函数根据系统资源是否用到定义,用到后需要深拷贝自定义构造函数;
17、析构函数和构造函数都是被对象调用的,不包括对象指针;临时对象也会调用构造函数和析构函数。还有就是析构函数是释放对象的,包含临时对象,如果想释放对象指针或其他指针,则需要在析构函数中显示定义,相当于free函数,对象指针以及其它类型的指针需要使用delete释放。
- #include <stdio.h>
- class Test
- {
- int mi;
- public:
- Test(int i)
- {
- mi = i;
- printf("Test(): %d ", mi);
- }
- ~Test()
- {
- printf("~Test(): %d ", mi);
- }
- };
- int main()
- {
- Test t(1);
- Test* pt = new Test(2);
- delete pt;
- return 0;
- }
//输出结果1 2 2 1
经典代码示例(一):开发一个 数组类解决 原生数组 的安全性问题。
经典代码示例(二):开发一个四则运算。
补充:
C++中有三种创建对象的方法
- #include <iostream>
- using namespace std;
- class A
- {
- private:
- int n;
- public:
- A(int m):n(m)
- { }
- ~A(){}
- };
- int main()
- {
- A a(1); //栈中分配
- A b = A(1); //栈中分配 或者直接 A b = 1;
- A* c = new A(1); //堆中分配
- delete c;
- return 0;
- }
- 第一种和第二种没什么区别,一个隐式调用,一个显式调用,两者都是在进程虚拟地址空间中的栈中分配内存,而第三种使用了new,在堆中分配了内存,而栈中内存的分配和释放是由系统管理,而堆中内存的分配和释放必须由程序员手动释放。采用第三种方式时,必须注意一下几点问题:
- new创建类对象需要指针接收,一处初始化,多处使用
- new创建类对象使用完需delete销毁
- new创建对象直接使用堆空间,而局部不用new定义类对象则使用栈空间
- new对象指针用途广泛,比如作为函数返回值、函数参数等
- 频繁调用场合并不适合new,就像new申请和释放内存一样
- 栈的大小远小于堆的大
- 栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率 比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在 堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会 分 到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。