面向对象与面向过程
OOP强调在运行阶段决策,面向过程强调在编译阶段决策。
OOP基本单位为对象,面向过程是函数。
基本数据类型大小
32位OS | 64位OS | |
---|---|---|
char | 1 | 1 |
short | 2 | 2 |
int | 4 | 4 |
long | 4 | 8 |
long long | 8 | 8 |
float | 4 | 4 |
double | 8 | 8 |
指针 | 4 | 8 |
C++内存分区
1)代码区:存放函数的二进制代码,由操作系统来进行管理。
2)全局区:存放全局变量、静态变量以及常量。(数据段(静态区),常量区)
3)堆区:由工程师分配(new)和释放(delete),若不释放,程序结束时由操作系统来回收。
4)栈区:由编译器自动分配释放,存放着函数参数,局部变量等。
变量的分类
按作用域
全局变量和局部变量
局部变量:在函数内定义的变量,作用域限于函数内
全局变量:在函数外定义的变量,它的作用域是从变量定义开始,到程序结束
如果非定义全局变量的文件中要使用该外部变量,要使用关键字extern声明变量才能够使用。
全局变量能由声明的文件中所有函数都能引用全局变量的值,而在一个函数中能够改变了变量的值会对其他函数产生影响,所以在使用中需要注意变 量的安全性,如果在多线程中需要全局变量则需要给全局变量操作的语句加上线程锁或者原子操作。
全局变量和局部变量同名时,在函数中全局变量隐藏,局部变量起作用。
按生存周期
静态变量和动态变量
静态变量:在运行期间由系统分配固定的存储空间。它可以是全局变量,也可以是局部变量。
静态变量使用关键字static修饰分为静态局部和静态全局。
静态局部变量:它和局部变量的区别在于函数退出时,变量始终存在,但不能被其他函数使用,当再次进入该函数时,能保存上次的结果继续使用。如果不赋初值,则自动赋初值0或空字符。
静态全局变量:只在定义它的源文件中可见,在其他源文件中不可见。它和全局变量的区别是,全局变量可以使用关键字extern被其他源文件使用,而静态全局变量不能。
动态变量
相对于静态变量的普通变量。
预处理
宏定义
定义常量
#define PI 3.14159265354
定义表达式
#define add(a,b) a+b
定义代码块
#define func(a) { \
printf("hello %s", a); \
}
文件包含
条件编译
为了减少可执行程序的大小,节省内存,提高效率
(1)
#ifdef <标识符>
// 程序段
#endif
(2)
#ifndef <标识符>
//程序段
#endif
(3)
#if (条件)
//程序段1
#elif (条件)
//程序段2
.......
#else
//程序段n
#endif
结构体
定义成员变量位宽
- 位结构中的成员不能使用数组和指针, 但位结构变量可以是数组和指针, 如果是指针, 其成员访问方式同结构指针。
- 位结构总长度(位数), 是各个位成员定义的位数之和, 可以超过两个字节。
- 位结构成员可以与其它结构成员一起使用。
C语言标准规定,只有有限的几种数据类型可以用于位域。在 ANSI C 中,这几种数据类型是 int、signed int 和 unsigned int(int 默认就是 signed int);到了 C99,_Bool 也被支持了。
但编译器在具体实现时都进行了扩展,额外支持了 char、signed char、unsigned char 以及 enum 类型,所以上面的代码虽然不符合C语言标准,但它依然能够被编译器支持。
结构体与共用体的区别
共用体(union)也可以存储多种数据类型的成员变量,单只能同时存储一种类型。
malloc与new的区别
1.malloc只能开辟内存不能初始化
int *p1 = (int*)malloc(sizeof(int));
=> 根据传入字节数开辟内存,没有初始化
int *p2 = new int(0);
=> 根据指定类型int开辟一个整形内存,初始化为0
int *p3 = (int*)malloc(sizeof(int)*100);
=> 开辟400个字节的内存,相当于包含100个整形元素的数组,没有初始化
int *p4 = new int[100]();
=> 开辟400个字节的内存,100个元素的整形数组,元素都初始化为0
int *p5 = new int[5]{ 1,2,3,4,5 };
=> 初始化数组为 1,2,3,4,5(VS2015支持)
int *p6 = new int[5]{ 1 };
=>初始化数组为 1,0,0,0,0
2.malloc是函数,开辟内存需要传入字节数,如malloc(100);表示在堆上开辟了100个字节的内存,返回void*,表示分配的堆内存的起始地址,因此malloc的返回值需要强转成指定类型的地址;new是运算符,开辟内存需要指定类型,返回指定类型的地址,因此不需要进行强转。
3.malloc开辟内存失败返回NULL,new开辟内存失败抛出bad_alloc类型的异常,需要捕获异常才能判断内存开辟成功或失败,new运算符其实是operator new函数的调用,它底层调用的也是malloc来开辟内存的,new它比malloc多的就是初始化功能,对于类类型来说,所谓初始化,就是调用相应的构造函数。
4.malloc开辟的内存永远是通过free来释放的;而new单个元素内存,用的是delete,如果new[]数组,用的是delete[]来释放内存的。
5.malloc开辟内存只有一种方式,而new有四种分别是普通的new(内存开辟失败抛出bad_alloc异常), nothrow版本的new,const new以及定位new。
结构体与类
- 继承访问权。
class默认的是private,struct默认的是public。**即class默认继承方式是private继承,而struct是public继承。所以我们在写类的时候都会要求我们指定是公有继承还是私有继承。 - 默认访问权限。
struct作为数据结构的实现体,它默认的数据访问控制是public的,而class作为对象的实现体,它默认的成员变量访问控制是private的。 - 模板参数
“class”这个关键字还用于定义模板参数,就像“typename”。但关键字“struct”不用于定义模板参数。
strcpy、memcpy、memset
strcpy拷贝字符串,遇到’\0’就结束拷贝;memcpy指定长度拷贝一段内存空间;memset用来对一段内存空间全部设置为某个字符,一般用于在对定义的字符串初始化为’ ‘或者’\0’。
内联函数
函数模板
template <typename T> //or class T
void Swap(T &,T &);
...
...
template <typename T> //or class T
void Swap(T &,T &)
{
...
}
-
可执行程序中是最终生成的函数。
-
模板定义也可重载
显式具体化
有些操作,比如修改结构体的某些成员,无法用模板,可用显式具体化
- 模板与具体化同时匹配时,优先使用显式具体化
template <typename T> //or class T
void Swap(T &a,T &b);
template <> void Swap<job>(job &,job &)
具体化
-
隐式实例化
调用模板以后生成函数定义。 -
显示实例化
-
显示具体化
函数指针
将一个函数的地址作为另一个函数的参数,跟直接调用的区别在于:
可以在不同时间传递不同函数的地址,从而可以不同时间使用不同的函数。
获取函数地址
process(think) 传递函数的地址。
process(think());传递函数返回值。
声明函数指针
声明指向某种数据类型的指针时,必须指定指针指向的类型。同样,声明指向函数的指针时,也必须指定指针指向的函数类型。
double pam(int);//函数原型
double (*pf)(int);//对应的函数指针
使用函数指针调用函数
pf=pam;
double x = pam(4);
double y = (*pf)(4);//等价于double y = pf(4);
引用变量与指针
- 引用作为变量的别名,而指针是一个实体。
- 声明 int rats;int & ro = rats;此处&类似指针的声明,并不是取地址的意思。
- 引用必须在声明时对其初始化,而且只能初始化一次,不可变;指针可以被重新赋值。
- 指针可以为空,引用不可能为空,所以指针需要做安全性检查防止空指针异常而引用不用。
- 引用可以干的事指针都可以干,但当你需要指向某个东西不再改变时,需求无法 由指针达成 ,例如vec[3]=3,可以使用引用。
单独编译
一般来说我们编写程序的时候是将不同功能的代码放在不同的文件中,c/c++编译器在编译的时候都是对每个文件进行单独编译的,然后进行链接。如果只修改了一个文件,编译器在重新编译的时候是只编译修改的文件,这样做的好处是节省时间,便于编译器进行管理。
说明符与限定符
存储说明符:
auto、register、static、extern、mutable、thread_local
mutable:结构或类为const,可用mutable来使某个成员可以被修改。
CV说明符:
const、volatile
volatile:即使程序代码没有对内存单元进行修改,其值也可能发生变化。听起来似乎很神秘,实际上并非如此。例如,可以将一个指针指向某个硬件位置,其中包含了来自串行端口的时间或信息。在这种情况下,硬件(而不是程序)可能修改其中的内容。
析构函数
如在建立对象时用new开辟了一片内存空间,delete会自动调用析构函数后释放内存。
作用域为类的常量
运算符重载
Sum()成员函数名称改为operator +()即可实现加法运算符重载;成员函数最后的const表明该函数不能修改类的其他成员。
友元
对私有成员的访问权限的扩展。
- 友元函数
- 友元类
友元函数
友元函数是可以直接访问类的私有成员的非成员函数。它是定义在类外的普通函数,它不属于任何类,但需要在类的定义中加以声明,声明时只需在友元的名称前加上关键字friend,其格式如下:
friend 类型 函数名(形式参数);
友元函数的声明可以放在类的私有部分,也可以放在公有部分,它们是没有区别的,都说明是该类的一个友元函数。
一个函数可以是多个类的友元函数,只需要在各个类中分别声明。
友元函数的调用与一般函数的调用方式和原理一致。
友元类
友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。
当希望一个类可以存取另一个类的私有成员时,可以将该类声明为另一类的友元类。定义友元类的语句格式如下:
friend class 类名;
其中:friend和class是关键字,类名必须是程序中的一个已定义过的类。
友元的注意项
使用友元类时注意:
(1) 友元关系不能被继承。
(2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。
(3) 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明
类的自动类型转换
下述构造函数可用于将double类型转换为Stone对象类型
Stone(double lbs);
也就是说可以这样编码:
Stone stone;
stone = 19.6;
只有接收一个参数的构造函数可以作为转换函数,有多个参数的除非其余参数都有默认值。
explicit 关键字可以关闭隐式自动类型转换
但仍可显式转换,语法如下:
stone = (Stone)19.6;
运算符重载
成员函数重载
(本质):Person p3 = p1.operator+(p2);
全局函数重载
(本质) : Person p3 = operator+(p1,p2);
运算符重载+函数重载
//运算符重载也可以发生函数重载
Person operator+(Person &p1,int num)
{
Person temp;
temp.m_A = p1.m_A + num;
temp.m_B = p1.m_B + num;
return temp;
}
多态
比如现在有一个基类,有两个派生类继承这个基类,并重写了基类的虚函数,现在定义了基类指针,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,我们将这种现象称为 多态
虚函数
虚函数的目的就是告诉编译器在编译期间不要确定函数的地址,在运行期间再根据指针实际指向的对象去确定函数地址,以此实现多态。
构成多态的条件
1.必须存在继承关系;
2.派生类重写基类的虚函数;
3.存在基类的指针指向派生类对象或者引用。
否则调用基类方法。
多态的好处
方便扩展,增加新的子类不影响已存在类的多态性、继承性,以及其他特性的运行和操作
析构函数
析构函数必须为虚函数——虚析构函数,析构函数默认不是虚函数(因为创建虚函数表会消耗内存)
但如果存在继承关系的话,析构函数就必须声明为虚函数,因为如果析构函数不被声明成虚函数,那么编译器将实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不会调用派生类析构函数,这样就会造成派生类对象析构不完全,造成内存泄漏
静态成员函数
只局限于本类对所有类有效,声明为虚函数,没有意义。
纯虚函数和抽象类
没有函数体的虚函数就是纯虚函数
语法格式为:
virtual 返回值类型 函数名 (函数参数) = 0;
包含纯虚函数的类称为抽象类,抽象类无法创建对象。原因很明显,纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。抽象类通常是作为基类,让派生类去实现纯虚函数。
意义:
1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。
2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理
多态的实现原理:虚函数表
编译器之所以能通过指针指向的对象找到虚函数,归功于虚函数表。
什么是虚函数表?
当一个类里存在虚函数时,编译器会为类创建一个虚函数表vtable,虚函数表是一个数组,数组的元素存放的是类中虚函数的地址。
编译器还会在对象的存储空间中安插一个指针vfptr,指向虚函数表数组的起始位置
通过虚函数表找到虚函数的过程:
编译器在类中添加了一个指针,*__vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。
当通过指针调用虚函数时,先根据指针找到对象,然后由对象中的vfptr找到虚函数表,通过虚函数表找到虚函数地址,即调用入口。
虚函数表是和类对应的,一个类有一个虚函数表,多个对象共享一个虚函数表,只要类有虚函数,编译时就会为该类创建虚函数表,即使没有new对象
静态联编与动态联编
通过函数调用来确定执行函数代码块的过程叫做函数名联编,重载使得这项工作更复杂,编译器可以在编译时确定,叫做静态联编;而多态无法在编译时进行这种联编,所以又有了在运行时确定的动态联编(晚期联编)。
类模板
例子:Stack类
template <class Type>
class Stack
{
public:
Type items[max];
...
...
}
//方法的定义要换成
返回值类型 Stack<Type>::方法名
...
...
//使用的时候:
Stack<int> s1;
Stack<string> s2;
友元
可将一个类声明为友元类,然后所有方法都可以访问原始类的私有成员和保护成员,声明的位置无所谓;
也可以只将类的特定的成员函数指定为另一个类的友元,但必须将友元函数所在类的定义放在原始类的前面。
嵌套类
嵌套类只是个在原始类内定义的友元类,嵌套类隐藏得比较深,且有权限访问所在类的一切成员,无论是私有的还是公有的。
嵌套类主要用于当该类仅仅被所在类使用,不需要外部进行显式地构造,且需要对所在类的成员进行大量访问操作的情况。
异常
- 可抛出字符串
- 可抛出对象,catch不同的对象执行不同的动作
- 如果不定义异常,则遇到异常时自动调用abort()函数,程序停止。