目录
1、static关键字
2、const 关键字
3、 const与#define
4、指针与引用的区别?
5、指针与数组的区别
6、堆与栈
7、new与malloc的区别
8、内存泄漏
9、空指针和野指针
10、虚函数与虚析构
11、纯虚函数与抽象类
12、深拷贝和浅拷贝
13、struct内存大小的确定
14、函数模板
15、普通函数与函数模板的区别
16、类模板与函数模板区别
17、STL六大组件
18、STL 迭代器 与指针的区别,迭代器怎么删除元素
1、static关键字
(1)在修饰局部变量的时候,static 修饰的局部变量只执行初始化一次,而且延长了局部变量的生命周期,直到程序运行结束以后才释放。
(2)static 修饰全局变量的时候,这个全局变量只能在本文件中访问,不能在其它文件中访问,即便是 extern 外部声明也不可以。
(3)static 修饰一个函数,则这个函数的只能在本文件中调用,不能被其他文件调用。static 修饰的变量存放在全局数据区的静态变量区,包括全局静态变量和局部静态变量,都在全局数据区分配内存。初始化的时候自动初始化为 0。
(4)不想被释放的时候,可以使用static修饰。比如修饰函数中存放在栈空间的数组。如果不想让这个数组在函数调用结束释放可以使用 static 修饰。
(5)考虑到数据安全性(当程序想要使用全局变量的时候应该先考虑使用 static)。
2、const 关键字
(1)关键字const用来定义只读变量,被const定义的变量它的值是不允许改变的,即不允许给它重新赋值,即使是赋相同的值也不可以。所以说它定义的是只读变量,这也就意味着必须在定义的时候就给它赋初值。
(2)const修饰指针有三种情况:
1. const修饰指针 --- 常量指针 : 指针指向可以修改,指针指向的值不可以修改
const int * p1 = &a; p1 = &b; //正确 //*p1 = 100; 报错
2. const修饰常量 --- 指针常量: 指针的指向不可以修改,指针指向的值可以修改
int * const p2 = &a; //p2 = &b; //错误 *p2 = 100; //正确
3. const即修饰指针,又修饰常量: 指针的指向和指向的值都不可以修改
const int * const p3 = &a; //p3 = &b; //错误 //*p3 = 100; //错误
(3)const 与 函数形参:const 通常用在函数形参中,在C标准库中有很多函数形参都用const限制了,为了防止在函数内部修改指针指向的数据,例如 fopen_s
(4)const修饰成员函数,表示指针指向的内存空间的数据不能修改,除了mutable修饰的变量,mutable在const函数中可以修改成员变量。
3、 const与#define
(1)、编译器处理方式:const:编译时确定其值;define:预处理时进行替换
(2)、类型检查:const:有数据类型,编译时进行数据检查;define:无类型,也不做类型检查
(3)、内存空间:const:在静态存储区储存,仅此一份;define:在代码段区,每做一次替换就会进行一次拷贝
(4)、define可以用来防止重复定义,const不行
(5)、define是预编译指令,而const是普通变量的定义。define定义的宏是在预处理阶段展开的,而const定义的只读变量是在编译运行阶段使用的。
(6)、const定义的是变量,而define定义的是常量。define定义的宏在编译后就不存在了,它不占用内存,因为它不是变量,系统只会给变量分配内存。但const定义的常变量本质上仍然是一个变量,具有变量的基本属性,有类型、占用存储单元。
4、指针与引用的区别?
(1)、指针:一个变量,存储的内容为一个地址;引用:给一个已有对象起的别名
(2)、指针是一个实体,需要分配内存空间;引用只是变量别名,不需要分配内存空间
(3)、可以有多级指针,不能有多级引用
(4)、自增运算结果不一样
(5)、指针是间接访问,引用是直接访问
(6)、指针可以不用初始化,引用一定要先初始化
5、指针与数组的区别
(1)、数组对应着一块内存,而指针是指向一块内存。数组的地址和空间大小在生命周期不会发生改变,内容可能会发生改变,而指针指向的内存大小可以随时发生改变。当指针指向常量字符串时,它的内容不可以改。
(2)、计算容量的区别:用sizeof计算出数组的元素个数,无法计算指针所指向内存的大小
(3)、数组名是常量指针,指针是变量指针
(4)、对数组用&和对指针&的意义不同,此时数组名不在当成指向一个元素的常量指针来使用
6、堆与栈
(1)、管理方式:栈由编译器管理,堆由程序员自己申请和释放;
(2)、大小限制:程序栈大小一般固定较小(几M),堆较大(几G);
(3)、分配方式:堆动态分配,栈一般是静态分配(如局部变量的定义)也有动态分配(不常用)
(4)、生长方向:栈向低地址端扩展;堆向高地址端扩展;
(5)、分配效率:栈由系统提供,甚至有专门的寄存器和指令负责,而堆有C++程序库实现,有复杂的堆算法,故栈效率要高于堆;
(6)、此外,堆的分配还会产生内碎片,影响存储内存的使用,栈先进后出结构,不会产生碎片。
7、new与malloc的区别
(1)、属性:new为关键字,malloc为库函数,需要头文件支持
(2)、参数:使用new申请内存无需指定内存大小,编译器会自行计算,而malloc需要显示的给出所需内存的大小
(3)、返回类型:new分配成功返回的是对象类型指针,与对象严格匹配,无需类型转换,故new是符合类型安全性操作符,malloc返回的是void*
(4)、分配失败:new分配失败,抛出bad_alloc异常,malloc则是返回NULL
(5)、内存区域:new分配的内存在自由储存区,malloc在堆上分配内存
8、内存泄漏
(1)、在内存中程序员手动分配的一块内存,mallocreallocnew。完成相关操作后,没有调用相对应的freedelete释放掉内存,这时这块内存就会常驻内存,造成堆内存泄漏,可以用智能指针解决。
(2)、Auto_ptr可以实现部分shared_ptr的功能,但已经被弃用。
(3)、Shared_ptr:多指针共享对象,存在引用计数,赋值给其他指针,计数增加;被赋值,计数减少;当引用计数为0,自带的销毁函数调用类析构函数析构对象;当循环引用时,仍然会发生内存泄漏;靠weak_ptr解决。
(4)、Weak_ptr:弱引用,被复制shared_ptr对象不会引发引用计数,能很好的解决shared循环引用内存泄露的问题。
(5)、Unique_ptr:任意时刻只能由一个指着对象,禁止复制和拷贝,实现形式“=delete”。计数为0,析构对象。
9、空指针和野指针
(1)、空指针:指针变量指向内存中编号为0的空间,用途为初始化指针变量,空指针指向的内存是不可以访问的。
int main()
{
//指针变量p指向内存地址编号为0的空间
int * p = NULL;
//访问空指针报错
//内存编号0 ~255为系统占用内存,不允许用户访问
cout << *p << endl;
system("pause");
return 0;
}
(2)、野指针:指向被释放的内存或者访问受限的指针
造成的原因:1、指针未被初始化 2、被释放的指针没有被置为NULL 3、指针越界操作
int main()
{
//指针变量p指向内存地址编号为0x1100的空间
int * p = (int *)0x1100;
//访问野指针报错
cout << *p << endl;
system("pause");
return 0;
}
注意:空指针和野指针都不是我们申请的空间,因此不要访问。
10、虚函数与虚析构
(1)、虚函数由virtual标识,采用动态绑定方式。派生类覆盖基类虚函数,基类指针指向派生类对象,实现动态多态。在运行时判断指针指向的对象类型从而调用该对象的函数版本。
(2)、虚函数允许我们在子类中重写方法,例如,假设我们有两个类A和B。B是A派生出来的,也就是B是A的子类。如果我们在A类中创建一个方法,标起为virtual,我们可以选择在B类中重写那个方法,让它做其他的事情。
(3)、虚函数出现的地方,虚函数引入了一种叫做Dynamic Dispatch(动态联编)的东西,它通常通过v表(虚函数表)来实现编译,v表就是一个表,它包含基类中所有虚函数的映射。这样我们可以在它运行时,将它们映射到正确的覆写(override)函数。
(4)如果想复写一个函数,必须将基类中的基函数标记为虚函数,虚函数也是多态的必要组成部分,虚函数必须实现,如果不实现,编译器将报错。
缺点:
(1)、我们需要额外的内存来存储v表,这样我们就可以分配到正确的函数,包括基类中要有一个成员指针,指向v表。
(2)、每次调用虚函数时,我们需要遍历这个表,来确定要映射到哪个函数,这是额外的性能损失
虚析构:存在继承并且析构函数需要用来析构资源时,析构函数一定要为虚函数,若使用父类指针指向子类,用delete析构函数时,只会调用父类析构函数,不会调用子类的析构函数,造成内存泄漏。
11、纯虚函数与抽象类
(1)、纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0” 即virtual void funtion1()=0;
(2)、编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。
(3)、定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。
(4)、纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。
抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,它处于继承层次结构的较上层。
(1)抽象类的定义: 称带有纯虚函数的类为抽象类。
(2)抽象类的作用:抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。所以派生类实际上刻画了一组子类的操作接口的通用语义,这些语义也传给子类,子类可以具体实现这些语义,也可以再将这些语义传给自己的子类。
(3)使用抽象类时注意:
• 抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。
• 抽象类是不能定义对象的。
12、深拷贝和浅拷贝
浅拷贝:只是拷贝了基本类型的数据,而引类型数据,复制后也是会发生引用,浅拷贝只是指向被复制的内存地址,如果原来对象被修改,那么浅拷贝出来的对象也会被修改。
深拷贝:在计算机中开辟一块新内存用于存放复制的对象。因此要用new或者malloc等。
class Person {
public:
//无参(默认)构造函数
Person() {
cout << "无参构造函数!" << endl;
}
//有参构造函数
Person(int age ,int height) {
cout << "有参构造函数!" << endl;
m_age = age;
m_height = new int(height);
}
//拷贝构造函数
Person(const Person& p) {
cout << "拷贝构造函数!" << endl;
//如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
m_age = p.m_age;
m_height = new int(*p.m_height);
}
//析构函数
~Person() {
cout << "析构函数!" << endl;
if (m_height != NULL)
{
delete m_height;
}
}
public:
int m_age;
int* m_height;
};
void test01()
{
Person p1(18, 180);
Person p2(p1);
cout << "p1的年龄: " << p1.m_age << " 身高: " << *p1.m_height << endl;
cout << "p2的年龄: " << p2.m_age << " 身高: " << *p2.m_height << endl;
}
int main() {
test01();
system("pause");
return 0;
}
总结:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数(深拷贝),防止浅拷贝带来的问题。
13、struct内存大小的确定
存在内存对齐的缘故,对于32位机器,是4字节对齐,64位机器是8字节对齐。
struct A
{
int a; 4个字节
char b; 1个字节
int c; 4个字节
}
对齐后:4 + 4 + 4 = 12字节
14、函数模板
模板定义:模板是实现代码重用机制的一种工具,它可以实现类型参数化,即把类型定义为参数,从而实现了真正的代码可重用性。
模板分类:函数模板和类模板。函数模板针对参数类型不同的函数;类模板仅针对数据成员和成员函数类型不同的类。
使用模板目的:让程序员编写与类型无关的代码,提高复用性。
注意:模板的声明或定义只能在全局,命名空间或类范围内进行。即不能在局部范围,函数内进行,如不能在main函数中声明或定义一个模板。
15、普通函数与函数模板的区别
(1)、普通函数调用时可以发生自动类型转换(隐式类型转换)
(2)、函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
(3)、如果利用显示指定类型的方式,可以发生隐式类型转换
//普通函数
int myAdd01(int a, int b)
{
return a + b;
}
//函数模板
template
T myAdd02(T a, T b)
{
return a + b;
}
//使用函数模板时,如果用自动类型推导,不会发生自动类型转换,即隐式类型转换
void test01()
{
int a = 10;
int b = 20;
char c = 'c';
cout << myAdd01(a, c) << endl; //正确,将char类型的'c'隐式转换为int类型 'c'对应 ASCII码 99
//myAdd02(a, c); // 报错,使用自动类型推导时,不会发生隐式类型转换
myAdd02(a, c); //正确,如果用显示指定类型,可以发生隐式类型转换
}
int main() {
test01();
system("pause");
return 0;
}
总结:建议使用显示指定类型的方式,调用函数模板,因为可以自己确定通用类型T
16、类模板与函数模板区别
类模板作用:建立一个通用类,类中的成员 数据类型可以不具体制定,用一个虚拟的类型来代表。
类模板与函数模板区别主要有两点:
1. 类模板没有自动类型推导的使用方式
2. 类模板在模板参数列表中可以有默认参数
#include//类模板 template class Person { public: Person(NameType name, AgeType age) { this->mName = name; this->mAge = age; } void showPerson() { cout << "name: " << this->mName << " age: " << this->mAge << endl; } public: NameType mName; AgeType mAge; }; //1、类模板没有自动类型推导的使用方式 void test01() { // Person p("孙悟空", 1000); // 错误 类模板使用时候,不可以用自动类型推导 Person p("孙悟空", 1000); //必须使用显示指定类型的方式,使用类模板 p.showPerson(); } //2、类模板在模板参数列表中可以有默认参数 void test02() { Person p("猪八戒", 999); //类模板中的模板参数列表 可以指定默认参数 p.showPerson(); } int main() { test01(); test02(); system("pause"); return 0; }
总结:
类模板使用只能用显示指定类型方式
类模板中的模板参数列表可以有默认参数
17、STL六大组件
STL大体分为六大组件,分别是:容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器
(1)、容器:各种数据结构,如vector、list、deque、set、map等,用来存放数据。
(2)、算法:各种常用的算法,如sort、find、copy、for_each等
(3)、迭代器:扮演了容器与算法之间的胶合剂。
(4)、仿函数:行为类似函数,可作为算法的某种策略。
(5)、适配器:一种用来修饰容器或者仿函数或迭代器接口的东西。
(6)、空间配置器:负责空间的配置与管理。
18、STL 迭代器 与指针的区别,迭代器怎么删除元素
迭代器是STL的关键所在,STL中心思想是把容器和算法分离开发,独立设计成泛型(类模板和函数模板),再用一种粘合剂将二者结合起来,迭代器就是该角色。
迭代器按照容器内部次序访问元素,而避免了暴露/考虑容器内不得结构。毕竟容器内部的实现各异,map/set族使用RB-tree,unordered族使用hashtable,vector使用内置数组,通过三个核心指针实现,queue和list使用双端队列deque实现。
例如:map的第一个元素是mostleft,最左节点,而vector第一个就是vec[0]使用指针必须要了解各个容器内部的实现,使用迭代器map.begin()/end(), vec.begin()/end()即可。



