指针和引用都是一种内存地址的概念,区别呢,指针是一个实体,引用只是一个别名。
在程序编译的时候,将指针和引用添加到符号表中。
指针它指向一块内存,指针的内容是所指向的内存的地址,在编译的时候,则是将“指针变量名-指针变量的地址”添加到符号表中,所以说,指针包含的内容是可以改变的,允许拷贝和赋值,有 const 和非 const 区别,甚至可以为空,sizeof 指针得到的是指针类型的大小。
而对于引用来说,它只是一块内存的别名,在添加到符号表的时候,是将"引用变量名-引用对象的地址"添加到符号表中,符号表一经完成不能改变,所以引用必须而且只能在定义时被绑定到一块内存上,后续不能更改,也不能为空,也没有 const 和非 const 区别,sizeof 引用得到代表对象的大小。另外在参数传递中,指针需要被解引用后才可以对对象进行操作,而直接对引用进行的修改会直接作用到引用对象上。
(1)栈:由编译器管理分配和回收,存放局部变量和函数参数。
(2)堆:由程序员管理,需要手动 new malloc delete free 进行分配和回收,空间较大,但可能会出现内存泄漏和空闲碎片的情况。
(3)全局/静态存储区:分为初始化和未初始化两个相邻区域,存储初始化和未初始化的全局变量和静态变量。
(4)常量存储区:存储常量,一般不允许修改。
(5)代码区:存放程序的二进制代码。
栈
(1)由编译器进行管理,在需要时由编译器自动分配空间,在不需要时候自动回收空间,一般保存的是局部变量和函数参数等。
(2)连续的内存空间,在函数调用的时候,首先入栈的主函数的下一条可执行指令的地址,然后是函数的各个参数。
(3)大多数编译器中,参数是从右向左入栈(原因在于采用这种顺序,是为了让程序员在使用C/C++的“函数参数长度可变”这个特性时更方便。如果是从左向右压栈,第一个参数(即描述可变参数表各变量类型的那个参数)将被放在栈底,由于可变参的函数第一步就需要解析可变参数表的各参数类型,即第一步就需要得到上述参数,因此,将它放在栈底是很不方便的。)本次函数调用结束时,局部变量先出栈,然后是参数,最后是栈顶指针最开始存放的地址,程序由该点继续运行,不会产生碎片。
(4)栈是高地址向低地址扩展,栈低高地址,空间较小。
堆:
(1)由程序员管理,需要手动 new malloc delete free 进行分配和回收,如果不进行回收的话,会造成内存泄漏的问题。
(2)不连续的空间,实际上系统中有一个空闲链表,当有程序申请的时候,系统遍历空闲链表找到第一个大于等于申请大小的空间分配给程序,一般在分配程序的时候,也会空间头部写入内存大小,方便 delete 回收空间大小。当然如果有剩余的,也会将剩余的插入到空闲链表中,这也是产生内存碎片的原因。
(3)堆是低地址向高地址扩展,空间交大,较为灵活。
(1)值传递:形参是实参的拷贝,函数内部对形参的操作并不会影响到外部的实参。
(2)指针传递:也是值传递的一种方式,形参是指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行操作。
(3)引用传递:实际上就是把引用对象的地址放在了开辟的栈空间中,函数内部对形参的任何操作可以直接映射到外部的实参上面。
都可以用来在堆上分配和回收空间。new /delete 是操作符,malloc/free 是函数。
(1)执行 new 实际上执行两个过程:1.分配未初始化的内存空间(malloc);2.使用对象的构造函数对空间进行初始化;返回空间的首地址。如果在第一步分配空间中出现问题,则抛出 std::bad_alloc 异常,或被某个设定的异常处理函数捕获处理;如果在第二步构造对象时出现异常,则自动调用 delete 释放内存。
(2)执行 delete 实际上也有两个过程:1. 使用析构函数对对象进行析构;2.回收内存空间(free)。
(3)以上也可以看出 new 和 malloc 的区别,new 得到的是经过初始化的空间,而 malloc 得到的是未初始化的空间。所以 new 是 new 一个类型,而 malloc 则是malloc 一个字节长度的空间。delete 和 free 同理,delete 不仅释放空间还析构对象,delete 一个类型,free 一个字节长度的空间。
(4)为什么有了 malloc/free 还需要 new/delete?因为对于非内部数据类型而言,光用 malloc/free 无法满足动态对象的要求。对象在创建的同时需要自动执行构造函数,对象在消亡以前要自动执行析构函数。由于 mallo/free 是库函数而不是运算符,不在编译器控制权限之内,不能够把执行的构造函数和析构函数的任务强加于 malloc/free,所以有了 new/delete 操作符。
volatile 三个特性
(1)易变性:在汇编层面反映出来,就是两条语句,下一条语句不会直接使用上一条语句对应的 volatile 变量的寄存器内容,而是重新从内存中读取。
(2)不可优化性:volatile 告诉编译器,不要对我这个变量进行各种激进的优化,甚至将变量直接消除,保证程序员写在代码中的指令,一定会被执行。
(3)顺序性:能够保证 volatile 变量之间的顺序性,编译器不会进行乱序优化。
extern
在 C 语言中,修饰符 extern 用在变量或者函数的声明前,用来说明 “此变量/函数是在别处定义的,要在此处引用”。
注意 extern 声明的位置对其作用域也有关系,如果是在 main 函数中进行声明的,则只能在 main 函数中调用,在其它函数中不能调用。其实要调用其它文件中的函数和变量,只需把该文件用 #include 包含进来即可,为啥要用 extern?因为用 extern 会加速程序的编译过程,这样能节省时间。
在 C++ 中 extern 还有另外一种作用,用于指示 C 或者 C++函数的调用规范。比如在 C++ 中调用 C 库函数,就需要在 C++ 程序中用 extern “C” 声明要引用的函数。这是给链接器用的,告诉链接器在链接的时候用C 函数规范来链接。主要原因是 C++ 和 C 程序编译完成后在目标代码中命名规则不同,用此来解决名字匹配的问题。
static 作用:控制变量的存储方式和可见性。
(1)修饰局部变量:一般情况下,对于局部变量在程序中是存放在栈区的,并且局部的生命周期在包含语句块执行结束时便结束了。但是如果用 static 关键字修饰的话,该变量便会存放在静态数据区,其生命周期会一直延续到整个程序执行结束。但是要注意的是,虽然用 static 对局部变量进行修饰之后,其生命周期以及存储空间发生了变化,但其作用域并没有改变,作用域还是限制在其语句块。
(2)修饰全部变量:对于一个全局变量,它既可以在本文件中被访问到,也可以在同一个工程中其它源文件被访问(添加 extern进行声明即可)。用 static 对全局变量进行修饰改变了其作用域范围,由原来的整个工程可见变成了本文件可见。
(3)修饰函数:用 static 修饰函数,情况和修饰全局变量类似,也是改变了函数的作用域。
(4)修饰类:如果 C++ 中对类中的某个函数用 static 修饰,则表示该函数属于一个类而不是属于此类的任何特定对象;如果对类中的某个变量进行 static 修饰,则表示该变量以及所有的对象所有,存储空间中只存在一个副本,可以通过;类和对象去调用。
(5)补充:静态非常量数据成员,其只能在类外定义和初始化,在类内仅是声明而已。
const 关键字:含义及实现机制
(1)const 修饰基本类型数据类型:基本数据类型,修饰符 const 可以用在类型说明符前,也可以用在类型说明符后,其结果是一样的。在使用这些常量的时候,只要不改变这些常量的值即可。
(2)const 修饰指针变量和引用变量:如果 const 位于小星星的左侧,则 const 就是用来修饰指针所指向的变量,即指针指向为常量;如果 const 位于小星星的右侧,则 const 就是修饰指针本身,即指针本身是常量。
(3)const 应用到函数中:作为参数的 const 修饰符:调用函数的时候,用相应的变量初始化 const 常量,则在函数体中,按照 const 所修饰的部分进行常量化,保护了原对象的属性。 [注意]:参数 const 通常用于参数为指针或引用的情况; 作为函数返回值的 const 修饰符:声明了返回值后,const 按照"修饰原则"进行修饰,起到相应的保护作用。
(4)const 在类中的用法:const 成员变量,只在某个对象生命周期内是常量,而对于整个类而言是可以改变的。因为类可以创建多个对象,不同的对象其 const 数据成员值可以不同。所以不能在类的声明中初始化 const 数据成员,因为类的对象在没有创建时候,编译器不知道 const 数据成员的值是什么。const 数据成员的初始化只能在类的构造函数的初始化列表中进行。const 成员函数:const 成员函数的主要目的是防止成员函数修改对象的内容。要注意,const 关键字和 static 关键字对于成员函数来说是不能同时使用的,因为 static 关键字修饰静态成员函数不含有 this 指针,即不能实例化,const 成员函数又必须具体到某一个函数。
(5)const 修饰类对象,定义常量对象:常量对象只能调用常量函数,别的成员函数都不能调用。
**(6)补充:**const 成员函数中如果实在想修改某个变量,可以使用 mutable 进行修饰。成员变量中如果想建立在整个类中都恒定的常量,应该用类中的枚举常量来实现或者 static const。
(7)C ++ 中的 const类成员函数(用法和意义)
常量对象可以调用类中的 const 成员函数,但不能调用非 const 成员函数; (原因:对象调用成员函数时,在形参列表的最前面加一个形参 this,但这是隐式的。this 指针是默认指向调用函数的当前对象的,所以,很自然,this 是一个常量指针 test * const,因为不可以修改 this 指针代表的地址。但当成员函数的参数列表(即小括号)后加了 const 关键字(void print() const;),此成员函数为常量成员函数,此时它的隐式this形参为 const test * const,即不可以通过 this 指针来改变指向对象的值。
非常量对象可以调用类中的 const 成员函数,也可以调用非 const 成员函数。
首先,C 和 C++ 在基本语句上没有过大的区别。
(1)C++ 有新增的语法和关键字,语法的区别有头文件的不同和命名空间的不同,C++ 允许我们自己定义自己的空间,C 中不可以。关键字方面比如 C++ 与 C 动态管理内存的方式不同,C++ 中在 malloc 和 free 的基础上增加了 new 和 delete,而且 C++ 中在指针的基础上增加了引用的概念,关键字例如 C++中还增加了 auto,explicit 体现显示和隐式转换上的概念要求,还有 dynamic_cast 增加类型安全方面的内容。
(2)函数方面:C++ 中有重载和虚函数的概念:
- C++ 支持函数重载而 C 不支持,是因为 C++ 函数的名字修饰与 C 不同,C++ 函数名字的修饰会将参数加在后面,例如,int func(int,double)经过名字修饰之后会变成_func_int_double,而 C 中则会变成 _func,所以 C++ 中会支持不同参数调用不同函数。
- C++ 还有虚函数概念,用以实现多态。
(3)类方面,C 的 struct 和 C++ 的类也有很大不同,C++ 中的 struct 不仅可以有成员变量还可以成员函数,而且对于 struct 增加了权限访问的概念,struct 的默认成员访问权限和默认继承权限都是 public,C++ 中除了 struct 还有 class 表示类,struct 和 class 还有一点不同在于 class 的默认成员访问权限和默认继承权限都是 private。
(4)C++ 中增加了模板还重用代码,提供了更加强大的 STL 标准库。
(5)最后补充一点就是 C 是一种结构化的语言,重点在于算法和数据结构。C 程序的设计首先考虑的是如何通过一个代码,一个过程对输入进行运算处理输出。而 C++ 首先考虑的是如何构造一个对象模型,让这个模型能够契合与之对应的问题领域,这样就能通过获取对象的状态信息得到输出。
(1)指针:Java 语言让程序员没法找到指针来直接访问内存,没有指针的概念,并有内存的自动管理功能,从而有效的防止了 C++ 语言中的指针操作失误的影响。但并非 Java 中没有指针,Java 虚拟机内部中还是用了指针,保证了 Java 程序的安全。
(2)多重继承:C++ 支持多重继承但 Java 不支持,但支持一个类继承多个接口,实现 C++ 中多重继承的功能,又避免了 C++ 的多重继承带来的不便。
(3)数据类型以类:Java 是完全面向对象的语言,所有的函数和变量必须是类的一部分。除了基本数据类型之外,其余的都作为类对象,对象将数据和方法结合起来,把它们封装在类中,这样每个对象都可以实现自己的特点和行为。Java 中取消了 C++ 中的 struct 和 union 。
(4)自动内存管理:Java 程序中所有对象都是用 new 操作符建立在内存堆栈上,Java 自动进行无用内存回收操作,不需要程序员进行手动删除。而 C++ 中必须由程序员释放内存资源,增加了程序设计者的负担。Java 中当一个对象不再被用到时, 无用内存回收器将给他们加上标签。Java 里无用内存回收程序是以线程方式在后台运行的,利用空闲时间工作来删除。
(5)Java 不支持操作符重载。操作符重载被认为是 C++ 的突出特性。
(6)Java 不支持预处理功能。C++ 在编译过程中都有一个预编译阶段,Java 没有预处理器,但它提供了 import 与 C++ 预处理器具有类似功能。
(7)类型转换:C++ 中有数据类型隐含转换的机制,Java 中需要限时强制类型转换。
(8)字符串:C++中字符串是以 Null 终止符代表字符串的结束,而 Java 的字符串 是用类对象(string 和 stringBuffer)来实现的。
(9)Java 中不提供 goto 语句,虽然指定 goto 作为关键字,但不支持它的使用,使程序简洁易读。
(10)Java 的异常机制用于捕获例外事件,增强系统容错能力。
对于 define 来说,宏定义实际上是在预编译阶段进行处理,没有类型,也就没有类型检查,仅仅做的是遇到宏定义进行字符串的展开,遇到多少次就展开多少次,而且这个简单的展开过程中,很容易出现边界效应,达不到预期的效果。因为 define 宏定义仅仅是展开,因此运行时系统并不为宏定义分配内存,但是从汇编 的角度来讲,define 却以立即数的方式保留了多份数据的拷贝。
对于 const 来说,const 是在编译期间进行处理的,const 有类型,也有类型检查,程序运行时系统会为 const 常量分配内存,而且从汇编的角度讲,const 常量在出现的地方保留的是真正数据的内存地址,只保留了一份数据的拷贝,省去了不必要的内存空间。而且,有时编译器不会为普通的 const 常量分配内存,而是直接将 const 常量添加到符号表中,省去了读取和写入内存的操作,效率更高。
按存储区域分:全局变量、静态全局变量和静态局部变量都存放在内存的静态存储区域,局部变量存放在内存的栈区。
按作用域分:全局变量在整个工程文件内都有效;静态全局变量只在定义它的文件内有效;静态局部变量只在定义它的函数内有效,只是程序仅分配一次内存,函数返回后,该变量不会消失;局部变量在定义它的函数内有效,但是函数返回后失效。
按初始值划分:全局变量和静态变量如果没有手工初始化,则由编译器初始化为0。局部变量的值不可知。
单例模式:
工厂模式:
工厂模式,包括简单工厂模式、抽象工厂模式、工厂方法模式。
- 简单工厂模式:主要用于创建对象。用一个工厂来根据输入的条件生成不同的类,然后根据不同类的虚函数得到不同的结果。
- 工厂方法模式:修正了简单工厂模式中不遵守开发封闭原则。把选择判断移到了客户端处理,如果要添加新的功能就不用修改原来的类,直接修改客户端即可。
- 抽象工厂模式:定义了一个创建一系列的相关或相互依赖的接口,而无需指定具体的类。
- 应用场景:适用于针对不同情况创建不同类时,只需传入工厂类的参数即可,无需了解具体实现方法。例如:计算器中对于同样的输入,执行不同的操作:加、减、乘、除。
虚函数: 被 virtual
关键字修饰的成员函数,就是虚函数。
#include <iostream>
using namespace std;
class A {
public:
virtual void v_fun() {
cout << "A::v_fun()" << endl;// 虚函数
}
};
class B : public A {
public:
void v_fun() {
cout << "B::v_fun()" << endl;
}
};
int main() {
A *p = new B();
p->v_fun(); // B::v_fun()
return 0;
}
纯虚函数:
- 纯虚函数在类中声明时,加上
=0
; - 含有纯虚函数的类称为抽象类(只要含有纯虚函数这个类就是抽象类),类中只有接口,没有具体的实现方法;
- 继承纯虚函数的派生类,如果没有完全实现基类纯虚函数,依然是抽象类,不能实例化对象。
说明:
- 抽象类对象不能作为函数的参数,不能创建对象,不能作为函数返回类型。
- 可以声明抽象类指针,可以声明抽象类的引用。
- 子类必须继承父类的纯虚函数,并全部实现后,才能创建子类的对象。
- 虚函数和纯虚函数可以出现在同一个类中,该类称为抽象基类。(含有纯虚函数的类为抽象基类)。
- 使用方式不同:虚函数可以直接使用,纯虚函数必须在派生类实现之后使用。
- 定义形式不同:虚函数在定义时在普通函数的基础上加上
virtual
关键字,纯虚函数定义时除了加上virtual
关键字还需要加上=0
; - 虚函数必须实现,否则编译器会报错。
- 对于实现纯虚函数的派生类,该纯虚函数在派生类中被称为虚函数,纯虚函数和虚函数都可以被重写。
- 析构函数最好定义为虚函数,特别是对于含有继承关系的类;析构函数可以定义为纯虚函数,此时,其所在的类为抽象基类,不能创建实例化对象。
**实现机制:**虚函数通过虚函数表来实现。虚函数的地址保存在虚函数表中,在类的对象所在的内存空间中,保存了指向虚函数表的指针(称为”虚表指针“),通过虚表指针可以找到类对应的虚函数表。虚函数表解决了基类和派生类的继承问题和类中成员函数的覆盖问题,当用基类的指针来操作一个派生类的时候,虚函数表就指明了实际应该调用的函数。
虚函数表相关知识点:
- 虚函数表存放的内容:类的虚函数的地址。
- 虚函数表建立的时间:编译阶段,程序的编译过程中会将虚函数的地址放在虚函数表中。
- 虚表指针保存的位置:虚表指针存放放在对象的内存空间中最前面的位置,这是为了保证正确取到虚函数地址。
虚函数表和类绑定,虚表指针和对象绑定。即类的不同的对象的虚函数表是一样的,但是每个对象都有自己的虚表指针,来指向类的虚函数表。
开篇,如何学好 Redis?