(1)指针有自己的一块空间,而引用只是一个别名;
(2)使用 sizeof 看一个指针的大小为4字节(32位,如果要是64位的话指针为8字节),而引用则是被引用对象的大小。
(3)指针可以被初始化为 NULL,而引用必须被初始化且必须是一个已有对象的引用。
(4)作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象。
(5)指针在使用中可以指向其他对象,但是引用只能是一个对象的引用,不能被改变。
(6)指针可以是多级,而引用没有分级
(7)如果返回动态分配内存的对象或者内存,必须使用指针,引用可能引起内存泄漏。
(1)堆栈空间分配区别:
(2)堆栈的缓存方式区别
(3)堆栈数据结构上的区别
堆(数据结构):堆可以被看成是一棵树,如:堆排序;
栈(数据结构):一种先进后出的数据结构。
3.1. new操作针对数据类型的处理,分为两种情况:
(1) 简单数据类型(包括基本数据类型和不需要构造函数的类型)
(2)复杂数据类型(需要由构造函数初始化对象)
3.2. delete也分为两种情况:
(1) 简单数据类型(包括基本数据类型和不需要析构函数的类型)
(2)复杂数据类型(需要由析构函数销毁对象)
3.3. new和delete与 malloc 和 free 的区别:
(1)属性上:new / delete 是c++关键字,需要编译器支持。 malloc/free是库函数,需要c的头文件支持。
(2)参数:使用new操作符申请内存分配时无须制定内存块的大小,编译器会根据类型信息自行计算。而mallco则需要显式地指出所需内存的尺寸。
(3)返回类型:new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,故new是符合类型安全性的操作符。而malloc内存成功分配返回的是void *,需要通过类型转换将其转换为我们需要的类型。
(4)分配失败时:new内存分配失败时抛出bad_alloc异常;malloc分配内存失败时返回 NULL。
(5)自定义类型:new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。 malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
(6)重载:C++允许重载 new/delete 操作符。而malloc为库函数不允许重载。
(7)内存区域:new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。其中自由存储区为:C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中。
C 是面向过程的一门编程语言,C++ 可以很好地进行面向对象的程序设计。C++ 虽然主要是以 C 的基础发展起来的一门新语言,但它不是 C 的替代品,它们是兄弟关系。面向对象和面向过程不是矛盾的,而是各有用途、互为补充的。
C++ 对 C 的增强,表现在六个方面:
(1)类型检查
C/C++ 是静态数据类型语言,类型检查发生在编译时,因此编译器知道程序中每一个变量对应的数据类型。C++ 的类型检查相对更严格一些。
很多时候需要一种能够实际表示多种类型的数据类型。传统上 C 使用 void* 指针指向不同对象,使用时强制转换回原始类型或兼容类型。这样做的缺陷是绕过了编译器的类型检查,如果错误转换了类型并使用,会造成程序崩溃等严重问题。
C++ 通过使用基类指针或引用来代替 void* 的使用,避免了这个问题(其实也是体现了类继承的多态性)。
面向对象
C 的结构体传递的是一种数据结构,我们只是在主函数里面对这种数据类型做某种调用。主函数的架构依然是基于函数、函数族的处理过程,即面向过程。
C++ 中最大的区别就是允许在结构体中封装函数,而在其他的地方直接调用这个函数。这个封装好的可直接调用的模块有个新名词——对象;并且也把结构体换一个名字——类。这就是面向对象的思想。在构建对象的时候,把对象的一些操作全部定义好并且给出接口的方式,对于外部使用者而言,可以不需要知道函数的处理过程,只需要知道调用方式、传递参数、返回值、处理结果。
泛型编程(template)
所谓泛型编程,简而言之就是不同的类型采用相同的方式来操作。在 C++ 的使用过程中,直接 template 用的不多,但是用 template 写的库是不可能不用的。因此需要对泛型有比较深入的了解,才可以更好地使用这些库。
C++ 里面的模版技术具有比类、函数更高的抽象水平,因为模版能够生成出(实例化)类和函数。可以用来:
异常处理
C 语言不提供对错误处理的直接支持,但它以返回值的形式允许程序员访问底层数据。在发生错误时,大多数的 C 或 UNIX 函数调用返回 1 或 NULL,同时会设置一个错误代码 errno,该错误代码是全局变量,表示在函数调用期间发生了错误。可以在 errno.h 头文件中找到各种各样的错误代码。
所以,C 程序员可以通过检查返回值,然后根据返回值决定采取哪种适当的动作。开发人员应该在程序初始化时,把 errno 设置为 0(表示没有错误),这是一种良好的编程习惯。
C++ 提供了一系列标准的异常,定义在 中,我们可以在程序中使用这些标准的异常。
函数重载 & 运算符重载
C++ 可以实现函数重载,条件是:函数名必须相同,返回值类型也必须相同,但参数的个数、类型或顺序至少有其一不同。
重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。大多数的重载运算符可被定义为普通的非成员函数(func(a, b) 形式调用)或者被定义为类成员函数(a.func(b) 形式调用)。
标准模板库(STL)
通常,我们聊到Java,第一印象“面向对象”,“没有指针,编写效率高,执行效率较低”。更深入、专业一点就谈论 “java内存自动回收(GC垃圾回收机制),多线程编程”。**
java的三大特性是封装、继承和多态。**
总结如下:
(1) JAVA的应用在高层,C++在中间件和底层
(2) JAVA离不开业务逻辑,而C++可以离开业务为JAVA们服务
(3) java语言给开发人员提供了更为简洁的语法;取消了指针带来更高的代码质量;完全面向对象,独特的运行机制是其具有天然的可移植性。
(4) java 是运行在JVM上的,之所以说它的可移植性强,是因为jvm可以安装到任何的系统
(5) c++不是不能在其他系统运行,而是c++在不同的系统上运行,需要不同的编码(这一点不如java,只编写一次代码,到处运行)。java程序一般都是生成字节码,在JVM里面运行得到结果。
(6) java 在web 应用上具有c++ 无可比拟的优势
(7) java在桌面程序上不如c++实用,C++可以直接编译成exe文件,指针是c++的优势,可以直接对内存的操作,但同时具有危险性 。(操作内存的确是一项非常危险的事情,一旦指针指向的位置发生错误,或者误删除了内存中某个地址单元存放的重要数据,后果是可想而知的)。
(8) 垃圾回收机制的区别。c++用析构函数回收垃圾,java自动回收(GC算法),写C和C++程序时一定要注意内存的申请和释放。
(9) java 丰富的插件是java 发展如此迅速的原因
(10)java 很大的沿袭了c++的一些实用结构
(11)对于底层程序的编程以及控制方面的编程,c++很灵活,因为有句柄的存在。Java并不仅仅是C++语言的一个变种,它们在某些本质问题上有根本的不同:
(1)首先说一下C中的结构体和C++中的结构体的异同:
(2)C++中 struct 与 class 的区别:
(1)起作用的阶段: #define是在编译的预处理阶段起作用,而const是在编译、运行的时候起作用。
(2)作用的方式:const常量有数据类型,而宏常量没有数据类型,只是简单的字符串替换。编译器可以对前者进行类型安全检查。而对后者没有类型安全检查,并且在字符替换时可能会产生意料不到的错误。
(3)存储的方式:#define只是进行展开,有多少地方使用,就替换多少次,它定义的宏常量在内存中有若干个备份;const定义的只读变量在程序运行过程中只有一份备份,const比较节省空间,避免不必要的内存分配,提高效率。
(1)static:
(2)const:
(1):在赋值方面,const数据成员只能采用初始化列表方式,而非数据成员可以采用初始化列表和构造函数体内赋值两种方式。
class Test
{
public:
Test(int x,int y):m_y(y)
{
m_x = x;//m_x也可以采用初始化列表方式,对于非内部数据类型最好采用初始化列表方式进行初始化
}
private:
int m_x;
const int m_y;
};
(2):在函数调用方面,const成员函数可以访问const数据成员(本身显示不能被修改)和const成员函数,可以访问非const数据成员,但是不能修改非const数据成员,且不能调用非const成员函数,而非const成员函数则没有限制。
const int* p2=&b; //-----可以改变p2的值,这是一个底层const
int* const p1=&i; //-----不能改变p1的值,这是一个顶层const
(1)final
struct A
{
//A::foo is final 限定该虚函数不能被重写
virtual void foo() final;
//Error: non-virtual function cannot be final,只能修改虚函数
void bar() final;
};
struct B final : A //struct B is final
{
//Error: foo cannot be overridden as it's final in A
void foo();
};
struct C : B //Error: B is final
{
};
(2)override
struct A
{
virtual void func() {}
};
struct D : A{
//显式重写
void func() override
{
}
};
(1)什么是拷贝初始化(也称为复制初始化):将一个已有的对象拷贝到正在创建的对象,如果需要的话还需要进行类型转换。拷贝初始化发生在下列情况:
(2)什么是直接初始化:在对象初始化时,通过括号给对象提供一定的参数,并且要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数
(3)直接初始化和拷贝初始化效率基本一样,因为在底层的实现基本一样,所以将拷贝初始化改为直接初始化效率提高不大。
(4) 例子
(1) 普通情况下,初始化和赋值好像没有什么特别去区分它的意义。
(2)复杂数据类型,如类,情况不同;
#include
using namespace std;
class Point
{
public:
Point(int a=0, int b=0):x(a), y(b){};
~Point(){
};
Point& operator =(const Point &rhs);
int x;
int y;
};
Point& Point::operator =(const Point &rhs)
{
x = rhs.x+1;
y = rhs.y+1;
return *this;
}
int main(void)
{
Point p(1,1);
Point p1 = p; //初始化操作
Point p2;
p2 = p; //赋值操作
cout<<"p1.x = "<<p1.x<<" "<<"p1.y="<<p1.y<<endl;
cout<<"p2.x = "<<p2.x<<" "<<"p2.y="<<p2.y<<endl;
return 0;
}
在p1中,Point p1=p;这个操作中,实际上是通过一种类似于拷贝构造函数中逐member的方式(但并没有生成一个拷贝构造函数,生成拷贝构造函数的四种情况见前面的随笔),并没有调用重载的"="运算符。所以最终结果是p1为(1,1)。
而在p2中,初始化与赋值是分开的,Point p2;就已经完成了初始化,这个初始化是通过定义的含参构造函数(但是以a=0,b=0的默认值完成的)。
然后在调用重载运算符,对p中成员均自加后赋值给p2的成员变量。
extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。
由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;
而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。比如说你用C 开发了一个DLL 库,为了能够让C ++语言也能够调用你的DLL输出(Export)的函数,你需要用extern "C"来强制编译器不要修改你的函数名。
引入的原因:编写单一的模板,它能适应大众化,使每种类型都具有相同的功能,但对于某种特定类型,如果要实现其特有的功能,单一模板就无法做到,这时就需要模板特例化。
定义:是对单一模板提供的一个特殊实例,它将一个或多个模板参数绑定到特定的类型或值上。
(1)函数模板特例化:必须为原函数模板的每个模板参数都提供实参,且使用关键字template后跟一个空尖括号对<>,表明将原模板的所有模板参数提供实参。
template<typename T> //函数模板
int compare(const T &v1,const T &v2)
{
if(v1 > v2) return -1;
if(v2 > v1) return 1;
return 0;
}
//模板特例化,满足针对字符串特定的比较,要提供所有实参,这里只有一个T
template<>
int compare(const char* const &v1,const char* const &v2)
{
return strcmp(p1,p2);
}
此处如果是compare(3,5),则调用普通的模板,若为compare(“hi”,”haha”)则调用特例化版本(因为这个cosnt char*相对于T,更匹配实参类型),注意,二者函数体的语句不一样了,实现不同功能。
(2)类模板的部分特例化:不必为所有模板参数提供实参,可以指定一部分而非所有模板参数,一个类模板的部分特例化本身仍是一个模板,使用它时还必须为其特例化版本中未指定的模板参数提供实参。此功能就用于STL源码剖析中的traits编程。详见C++primer 628页的例子。(特例化时类名一定要和原来的模板相同,只是参数类型不同,按最佳匹配原则,那个最匹配,就用相应的模板)
template<typename T>class Foo
{
void Bar();
void Barst(T a)();
};
template<>
void Foo<int>::Bar()
{
//进行int类型的特例化处理
}
Foo<string> fs;
Foo<int> fi;//使用特例化
fs.Bar();//使用的是普通模板,即Foo::Bar()
fi.Bar();//特例化版本,执行Foo::Bar()
//Foo::Bar()和Foo::Bar()功能不同
hash_table是STL中hash_map 和 hash_set 的内部数据结构,hash_table的插入/删除/查找的时间复杂度都为O(1),是查找速度最快的一种数据结构,但是hash_table中的数据是无序的,一般也只有在数据不需要排序,只需要满足快速查找/插入/删除的时候使用hash_table。
map是一种映射,这种映射是有序的,底层是使用红黑树来完成的,数据通过键值才存储,键是唯一的。
unordered_map,是一种无序的,底层是通过hash表来完成的。unordered库使用“桶”来存储元素,散列值相同的被存储在一个桶里。当散列容器中有大量数据时,同一个桶里的数据也会增多,造成访问冲突,降低性能。为了提高散列容器的性能,unordered库会在插入元素是自动增加桶的数量,不需要用户指定。每个桶都是用list来完成的。
(1)map
优点:
缺点:
适用于:
(2)unordered_map
(1).vector有备用空间,当备用空间不够的时候,会重新开辟原空间两倍的空间进行重写分配。
(2).vector支持随机的存取,但是最好是选择从末尾插入,因为从中间插入会导致元素的移动,带来了性能的开销。
顺序容器:vector,deque是随机访问迭代器;list是双向迭代器
容器适配器:stack,queue,priority_queue没有迭代器
关联容器:set,map,multiset,multimap是双向迭代器
unordered_set,unordered_map,unordered_multiset,unordered_multimap是前向迭代器
https://www.cnblogs.com/zhuwbox/p/3698083.html STL中的Traits编程技法
vector压入容器的对象都是拷贝操作,而且vector的数据存放都是连续存储的,所以在操作vector操作时,应该尽量避免对尾部操作之后的地方插入删除操作,因为这样会造成元素的移动,造成大量的开销。
频繁对vector调用push_back()会导致性能下降,这是由于系统每次给vector分配固定大小的空间,这个空间可能比用户想分配的空间大一些,但是频繁的使用push_back向容器中插入元素,会导致内存分配空间不够,会再次将整个对象的存储空间重新分配,将旧的元素移动到新的空间中,开销是非常大的。
c++的内存管理延续c语言的内存管理,但是也增加了其他的,例如智能指针,除了常见的堆栈的内存管理之外,c++支持智能指针,智能指针的对象进行赋值拷贝等操作的时候,每个智能指针都有一个关联的计数器,该计数器记录共享该对象的指针个数,当最后一个指针被销毁的时候,计数器为0,会自动调用析构函数来销毁函数。
常见的内存管理错误有:
a>内存分配未成功却使用了它,如果所用的操作符不是类型安全的话,请使用assert(p != NULL)或者if(p != NULL)来判断。
b>内存分配成功但未初始化
c>内存分配成功并已初始化,但是操作超过了内存的边界
d>忘记释放内存,造成内存泄露,每申请一块内存必须保证它被释放,释放内存后立即将指针置为NULL
数组与指针的对比,数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。
指针可以随时指向任意类型的内存块,它的特征是“可变”,所以我们常用指针来操作动态内存。指针远比数组灵活,但也更危险。
(1)内存池简介
C/C++下内存管理是让几乎每一个程序员头疼的问题,分配足够的内存、追踪内存的分配、在不需要的时候释放内存——这个任务相当复杂。而直接使用系统调用malloc/free、new/delete进行内存分配和释放,有以下弊端:
调用malloc/new,系统需要根据“最先匹配”、“最优匹配”或其他算法在内存空闲块表中查找一块空闲内存,调用free/delete,系统可能需要合并空闲内存块,这些会产生额外开销
频繁使用时会产生大量内存碎片,从而降低程序运行效率
容易造成内存泄漏
(2)内存池的优点
内存池则是在真正使用内存之前,预先申请分配一定数量、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的一个显著优点是,使得内存分配效率得到提升。
(3)内存池的分类
从线程安全的角度来分,内存池可以分为单线程内存池和多线程内存池。单线程内存池整个生命周期只被一个线程使用,因而不需要考虑互斥访问的问题;多线程内存池有可能被多个线程共享,因此需要在每次分配和释放内存时加锁。相对而言,单线程内存池性能更高,而多线程内存池适用范围更加广泛。
从内存池可分配内存单元大小来分,可以分为固定内存池和可变内存池。所谓固定内存池是指应用程序每次从内存池中分配出来的内存单元大小事先已经确定,是固定不变的;而可变内存池则每次分配的内存单元大小可以按需变化,应用范围更广,而性能比固定内存池要低。
(4)经典内存池的设计
经典内存池实现过程
a.先申请一块连续的内存空间,该段内存空间能够容纳一定数量的对象;
b.每个对象连同一个指向下一个对象的指针一起构成一个内存节点(Memory Node)。各个空闲的内存节点通过指针形成一个链表,链表的每一个内存节点都是一块可供分配的内存空间;
c.某个内存节点一旦分配出去,从空闲内存节点链表中去除;
d.一旦释放了某个内存节点的空间,又将该节点重新加入空闲内存节点链表;
e.如果一个内存块的所有内存节点分配完毕,若程序继续申请新的对象空间,则会再次申请一个内存块来容纳新的对象。新申请的内存块会加入内存块链表中。
经典内存池的实现过程大致如上面所述,其形象化的过程如下图所示:
如上图所示,申请的内存块存放三个可供分配的空闲节点。空闲节点由空闲节点链表管理,如果分配出去,将其从空闲节点链表删除,如果释放,将其重新插入到链表的头部。如果内存块中的空闲节点不够用,则重新申请内存块,申请的内存块由内存块链表来管理。
注意,本文涉及到的内存块链表和空闲内存节点链表的插入,为了省去遍历链表查找尾节点,便于操作,新节点的插入均是插入到链表的头部,而非尾部。当然也可以插入到尾部,读者可自行实现。
经典内存池数据结构设计
按照上面的过程设计,内存池类模板有这样几个成员。
两个指针变量:
内存块链表头指针:pMemBlockHeader;
空闲节点链表头指针:pFreeNodeHeader;
空闲节点结构体:
struct FreeNode
{
FreeNode* pNext;
char data[ObjectSize];
};
内存块结构体:
struct MemBlock
{
MemBlock *pNext;
FreeNode data[NumofObjects];
};
经典内存池的实现
根据以上经典内存池的设计,编码实现如下
#include <iostream>
using namespace std;
template<int ObjectSize, int NumofObjects = 20>
class MemPool
{
private:
//空闲节点结构体
struct FreeNode
{
FreeNode* pNext;
char data[ObjectSize];
};
//内存块结构体
struct MemBlock
{
MemBlock* pNext;
FreeNode data[NumofObjects];
};
FreeNode* freeNodeHeader;
MemBlock* memBlockHeader;
public:
MemPool()
{
freeNodeHeader = NULL;
memBlockHeader = NULL;
}
~MemPool()
{
MemBlock* ptr;
while (memBlockHeader)
{
ptr = memBlockHeader->pNext;
delete memBlockHeader;
memBlockHeader = ptr;
}
}
void* malloc();
void free(void*);
};
//分配空闲的节点
template<int ObjectSize, int NumofObjects>
void* MemPool<ObjectSize, NumofObjects>::malloc()
{
//无空闲节点,申请新内存块
if (freeNodeHeader == NULL)
{
MemBlock* newBlock = new MemBlock;
newBlock->pNext = NULL;
freeNodeHeader=&newBlock->data[0]; //设置内存块的第一个节点为空闲节点链表的首节点
//将内存块的其它节点串起来
for (int i = 1; i < NumofObjects; ++i)
{
newBlock->data[i - 1].pNext = &newBlock->data[i];
}
newBlock->data[NumofObjects - 1].pNext=NULL;
//首次申请内存块
if (memBlockHeader == NULL)
{
memBlockHeader = newBlock;
}
else
{
//将新内存块加入到内存块链表
newBlock->pNext = memBlockHeader;
memBlockHeader = newBlock;
}
}
//返回空节点闲链表的第一个节点
void* freeNode = freeNodeHeader;
freeNodeHeader = freeNodeHeader->pNext;
return freeNode;
}
//释放已经分配的节点
template<int ObjectSize, int NumofObjects>
void MemPool<ObjectSize, NumofObjects>::free(void* p)
{
FreeNode* pNode = (FreeNode*)p;
pNode->pNext = freeNodeHeader; //将释放的节点插入空闲节点头部
freeNodeHeader = pNode;
}
class ActualClass
{
static int count;
int No;
public:
ActualClass()
{
No = count;
count++;
}
void print()
{
cout << this << ": ";
cout << "the " << No << "th object" << endl;
}
void* operator new(size_t size);
void operator delete(void* p);
};
//定义内存池对象
MemPool<sizeof(ActualClass), 2> mp;
void* ActualClass::operator new(size_t size)
{
return mp.malloc();
}
void ActualClass::operator delete(void* p)
{
mp.free(p);
}
int ActualClass::count = 0;
int main()
{
ActualClass* p1 = new ActualClass;
p1->print();
ActualClass* p2 = new ActualClass;
p2->print();
delete p1;
p1 = new ActualClass;
p1->print();
ActualClass* p3 = new ActualClass;
p3->print();
delete p1;
delete p2;
delete p3;
}
程序运行结果:
004AA214: the 0th object
004AA21C: the 1th object
004AA214: the 2th object
004AB1A4: the 3th object
程序分析
阅读以上程序,应注意以下几点。
(1)对一种特定的类对象而言,内存池中内存块的大小是固定的,内存节点的大小也是固定的。内存块在申请之初就被划分为多个内存节点,每个Node的大小为ItemSize。刚开始,所有的内存节点都是空闲的,被串成链表。
(2)成员指针变量memBlockHeader是用来把所有申请的内存块连接成一个内存块链表,以便通过它可以释放所有申请的内存。freeNodeHeader变量则是把所有空闲内存节点串成一个链表。freeNodeHeader为空则表明没有可用的空闲内存节点,必须申请新的内存块。
(3)申请空间的过程如下。在空闲内存节点链表非空的情况下,malloc过程只是从链表中取下空闲内存节点链表的头一个节点,然后把链表头指针移动到下一个节点上去。否则,意味着需要一个新的内存块。这个过程需要申请新的内存块切割成多个内存节点,并把它们串起来,内存池技术的主要开销就在这里。
(4)释放对象的过程就是把被释放的内存节点重新插入到内存节点链表的开头。最后被释放的节点就是下一个即将被分配的节点。
(5)内存池技术申请/释放内存的速度很快,其内存分配过程多数情况下复杂度为O(1),主要开销在freeNodeHeader为空时需要生成新的内存块。内存节点释放过程复杂度为O(1)。
(6) 在上面的程序中,指针p1和p2连续两次申请空间,它们代表的地址之间的差值为8,正好为一个内存节点的大小(sizeof(FreeNode))。指针p1所指向的对象被释放后,再次申请空间,得到的地址与刚刚释放的地址正好相同。指针p3多代表的地址与前两个对象的地址相聚很远,原因是第一个内存块中的空闲内存节点已经分配完了,p3指向的对象位于第二个内存块中。
以上内存池方案并不完美,比如,只能单个单个申请对象空间,不能申请对象数组,内存池中内存块的个数只能增大不能减少,未考虑多线程安全等问题。现在,已经有很多改进的方案,请读者自行查阅相关资料。
注:与深入理解计算机系统(csapp)中几种内存分配方式对比学习加深理解
面向对象的三大特性:封装、继承、多态。
(1)封装:将很多有相似特性的内容封装在一个类中,例如学生的成绩学号、课程这些可以封装在同一个类中;
(2)继承:某些相似的特性,可以从一个类继承到另一个类,类似生活中的继承,例如有个所有的汽车都有4个轮子,那么我们在父类中定义4个轮子,通过继承获得4个轮子的功能,不用再类里面再去定义这4个轮子的功能。
(3)多态:多态指的相同的功能,不同的状态,多态在面向对象c++里面是通过重载和覆盖来完成的,覆盖在c++里面通过虚函数来完成的。例如鸭子的例子,所有的鸭子都有颜色,我们可以将这个颜色设置成为一个虚函数,通过继承子类对虚函数进行覆盖,不同子类中有各自的颜色,也就是有各自不同的鸭子颜色,这就是多态的典型表现之一。
多态用虚函数来实现,结合动态绑定。
引用/指针的静态类型与动态类型不同这一事实正是C++语言支持多态性的根本所在。
C++的多态性用一句话概括就是:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。
存在虚函数的类都有一个一维的虚函数表叫做虚表,类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的。
注意 :
①每个虚表后面都有一个‘0’,它类似字符串的‘\0’,用来标识虚函数表的结尾。结束标识在不同的编译器下可能会有所不同。
②不难发现虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
多态实现利用到了虚函数表(虚表V-table)。它是一块虚函数的地址表,通过一块连续内存来存储虚函数的地址。这张表解决了继承、虚函数(重写)的问题。在有虚函数的对象实例中都存在一张虚函数表,虚函数表就像一张地图,指明了实际应该调用的虚函数函数。
为什么基类指针指向派生类对象时可以调用派生类成员函数
虚表指针一般放在首地址,如果父类有虚函数表,子类必定有;因为构造子类时先构造父类,所以使用父类的指针,编译器根据指针类型就能知道偏移多少就能找到父类的成员(包括虚函数指针),但是对于子类独有的成员,父类的指针无法提供偏移量,因此找不到。
基类的虚函数存放在内存的什么区
全局数据区(静态区)
虚表指针vptr的初始化时间
所有基类构造函数之后,但又在自身构造函数或初始化列表之前
编译器处理虚函数的方法是:
编译器为每个包含虚函数的类创建一个表,在表中编译器放置特定类的虚函数地址,在每个带有虚函数的类中,编译器为每个类对象放置一个指针(为每个类添加一个隐藏的成员),指向虚表。通过基类的指针或引用做虚函数调用时,编译器静态插入取得该指针,并在虚表中找到函数地址。注意基类和派生类的虚函数表是俩个东西,保存在不同的空间,但这俩个东西的内容可能一样。
虚函数实现原理(包括单一继承,多重继承等):虚函数表+虚表指针
每个虚函数都会有一个与之对应的虚函数表,该虚函数表的实质是一个指针数组,存放的是每一个对象的虚函数入口地址。对于一个派生类来说,他会继承基类的虚函数表同时增加自己的虚函数入口地址,如果派生类重写了基类的虚函数的话,那么继承过来的虚函数入口地址将被派生类的重写虚函数入口地址替代。那么在程序运行时会发生动态绑定,将父类指针绑定到实例化的对象实现多态。
单继承环境下的虚函数
假设存在下面的两个类Base和A,A类继承自Base类:
class Base
{
public:
// 虚函数func1
virtual void func1() { cout << "Base::func1()" << endl; }
// 虚函数func2
virtual void func2() { cout << "Base::func2()" << endl; }
// 虚函数func3
virtual void func3() { cout << "Base::func3()" << endl; }
int a;
};
class A : public Base
{
public:
// 重写父类虚函数func1
void func1() { cout << "A::func1()" << endl; }
void func2() { cout << "A::func2()" << endl; }
// 新增虚函数func4
virtual void func4() { cout << "A::func3()" << endl; }
};
利用Visual Studio提供的命令行工具查看一下这两个类的内存布局。
类Base的内存布局图:
类A的内存布局图:
通过两幅图片的对比,我们可以看到:
在单继承中,A类覆盖了Base类中的同名虚函数,在虚函数表中体现为对应位置被A类中的新函数替换,而没有被覆盖的函数则没有发生变化。
对于子类自己的虚函数,直接添加到虚函数表后面。
另外,我们注意到,类A和类Base中都只有一个vfptr指针,前面我们说过,该指针指向虚函数表,我们分别输出类A和类Base的vfptr:
int main()
{
typedef void(*pFunc)(void);
cout << "virtual function testing:" << endl;
Base b;
cout << "Base虚函数表地址:" << (int *)(&b) << endl;
A a;
cout << "A类虚函数表地址:" << (int *)(&a) << endl;
}
输出信息如下:
我们可以看到,类A和类B分别拥有自己的虚函数表指针vptr和虚函数表vtbl。到这里,你是否已经明白为什么指向子类实例的基类指针可以调用子类(虚)函数?每一个实例对象中都存在一个vptr指针,编译器会先取出vptr的值,这个值就是虚函数表vtbl的地址,再根据这个值来到vtbl中调用目标函数。所以,只要vptr不同,指向的虚函数表vtbl就不同,而不同的虚函数表中存放着对应类的虚函数地址,这样就实现了多态的”效果“。
最后,我们用一幅图来表示单继承下的虚函数实现:
多继承环境下的虚函数
假设存在下面这样的四个类:
class Base
{
public:
// 虚函数func1
virtual void func1() { cout << "Base::func1()" << endl; }
// 虚函数func2
virtual void func2() { cout << "Base::func2()" << endl; }
// 虚函数func3
virtual void func3() { cout << "Base::func3()" << endl; }
};
class A : public Base
{
public:
// 重写父类虚函数func1
void func1() { cout << "A::func1()" << endl; }
void func2() { cout << "A::func2()" << endl; }
};
class B : public Base
{
public:
void func1() { cout << "B::func1()" << endl; }
void func2() { cout << "B::func2()" << endl; }
};
class C : public A, public B
{
public:
void func1() { cout << "D::func1()" << endl; }
void func2() { cout << "D::func2()" << endl; }
};
类A和类B分别继承自类Base,类C继承了类B和类A,我们查看一下类C的内存布局:
可以看到,类C中拥有两个虚函数表指针vptr。类C中覆盖了类A的两个同名函数,在虚函数表中体现为对应位置替换为C中新函数;类C中覆盖了类B中的两个同名函数,在虚函数表中体现为对应位置替换为C中新函数(注意,这里使用跳转语句,而不是重复定义)。
类C的内存布局可以归纳为下图:
多重继承会有多个虚函数表,几重继承,就会有几个虚函数表。这些表按照派生的顺序依次排列,如果子类改写了父类的虚函数,那么就会用子类自己的虚函数覆盖虚函数表的相应的位置,如果子类有新的虚函数,那么就添加到第一个虚函数表的末尾。
C++类成员所占内存总结:
(1)空类所占字节数为1
(2)类中的成员函数不占内存空间,虚函数除外;如果父类中如果有一个虚函数,则类所字节发生变化,如果是32位编译器,则占内存4个字节;如果是64位编译器,则占内存8个字节;
(3)和结构体一样,类中自身带有四字节对齐功能
(4)类中的static静态成员变量不占内存,静态成员变量存储在静态区
https://www.cnblogs.com/hnfxs/p/5395015.html C++类内存布局图(成员函数和成员变量分开讨论)
用类去定义对象时,系统会为每一个对象分配存储空间。如果一个类包括了数据和函数,要分别为数据和函数的代码分配存储空间。按理说,如果用同一个类定义了10个对象,那么就需要分别为10个对象的数据和函数代码分配存储单元,如下图所示。
我们可以看出这样不仅麻烦而且特别浪费空间,因此经过分析我们可以知道是按以下方式来储存的。
只用一段空间来存放这个共同的函数代码段,在调用各对象的函数时,都去调用这个公用的函数代码。如下图所示。
显然,这样做会大大节约存储空间。C++编译系统正是这样做的,因此每个对象所占用的存储空间只是该对象的数据部分(虚函数指针和虚基类指针也属于数据部分)所占用的存储空间,而不包括函数代码所占用的存储空间。
那么问题来了在不同对象但是调用的的代码又相同的情况下,编译器是如何分辨且准确的调用到各自的函数???
在c++中专门设立了一个this指针,用来指向不同的对象,当调用对象t1的成员函数display1时,this指针就指向display1,当调用t2的成员函数display2,this指针就指向display2。。。。。以此类推来分辨准确的调用
#include
using namespace std;
class Person
{
public:
virtual ~Person() //加了virtual,讲析构函数声明为虚函数
{
cout << "Person::~Person()" << endl;
}
};
class Student : public Person
{
public:
~Student() // virtual可加可不加
{
cout << "Student::~Student()" << endl;
}
};
int main()
{
Person *pt1 = new Person;
Person *pt2 = new Student; // 用基类的指针指向子类
// Student *pt3 = new Person; // 不能用子类指针指向基类,错误!
Student *pt4 = new Student;
delete pt1;
cout << "*********" << endl;
delete pt2;
cout << "*********" << endl;
//delete pt3;
//cout << "*********" << endl;
delete pt4;
cout << "*********" << endl;
return 0;
}
运行结果: Person::~Person()
Student::~Student()
Person::~Person()
Student::~Student()
Person::~Person()
如果在基类中析构函数不加virtual,结果为:
Person::~Person()
Person::~Person()
Student::~Student()
Person::~Person()
可以看出:只有在用基类的指针指向派生类的时候虚函数发挥了动态的作用。
析构函数执行时先调用派生类的析构函数,其次才调用基类的析构函数。
如果析构函数不是虚函数,而程序执行时又要通过基类的指针去销毁派生类的动态对象,那么用delete销毁对象时,只调用了基类的析构函数,未调用派生类的析构函数。这样会造成销毁对象不完全,容易造成内存泄露。
对象不存在,且没用别的对象来初始化,就是调用了构造函数;
对象不存在,且用别的对象来初始化,就是拷贝构造函数(上面说了三种用它的情况!)
对象存在,用别的对象来给它赋值,就是赋值函数。
C++中, 一个参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参构造函数), 承担了两个角色。 1 是个构造器 ,2 是个默认且隐含的类型转换操作符。
从C++之父Bjarne的回答我们应该知道C++为什么不支持构造函数是虚函数了,简单讲就是没有意义。虚函数的作用在于通过子类的指针或引用来调用父类的那个成员函数。而构造函数是在创建对象时自己主动调用的,不可能通过子类的指针或者引用去调用。
虚函数相应一个指向vtable虚函数表的指针,但是这个指向vtable的指针事实上是存储在对象的内存空间的。假设构造函数是虚的,就须要通过vtable来调用,但是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。
总的来说,构造函数和析构函数调用虚函数并不能达到多态的效果,因为在析构和构造过程中,该对象变为一个基类对象,调用的方法都是基类的方法。
纯虚函数不需要定义,我们不能够为纯虚函数提供函数体,同样的,包含纯虚函数的基类是抽象基类,抽象基类是不能创建对象的,只能通过继承,继承子类中覆盖纯虚函数,执行自己的功能,子类是可以创建对象的。
静态类型和动态类型:
对象的静态类型:
对象在声明是采用的类型,在编译期确定;
对象的动态类型:
当前对象所指的类型,在运行期决定,对象的动态类型可以更改,但静态类型无法更改。
静态绑定和动态绑定:
静态绑定:
绑定的是对象的静态类型,某特性(比如函数)依赖于对象的静态类型,发生在编译期。
动态绑定:
绑定的是对象的动态类型,某特性(比如函数)依赖于对象的动态类型,发生在运行期。
可以实现,因为动态绑定是发生在程序运行阶段的,c++中动态绑定是通过对基类的引用或者指针调用虚函数时发生。
因为引用或者指针的对象是可以在编译的时候不确定的,如果是直接传对象的话,在程序编译的阶段就会完成,对于引用,其实就是地址,在编译的时候可以不绑定对象,在实际运行的时候,在通过虚函数绑定对象即可。
深拷贝就是拷贝内容,浅拷贝就是拷贝指针。
浅拷贝拷贝指针,也就是说同一个对象,拷贝了两个指针,指向了同一个对象,那么当销毁的时候,可能两个指针销毁,就会导致内存泄漏的问题。
深拷贝不存在这个问题,因为是首先申请和拷贝数据一样大的内存空间,把数据复制过去。这样拷贝多少次,就有多少个不同的内存空间,干扰不到对方。
对象复用指得是设计模式,对象可以采用不同的设计模式达到复用的目的,最常见的就是继承和组合模式了。
零拷贝主要的任务就是避免CPU将数据从一块存储拷贝到另外一块存储,主要就是利用各种零拷贝技术,避免让CPU做大量的数据拷贝任务,减少不必要的拷贝,或者让别的组件来做这一类简单的数据传输任务,让CPU解脱出来专注于别的任务。这样就可以让系统资源的利用更加有效。
零拷贝技术常见linux中,例如用户空间到内核空间的拷贝,这个是没有必要的,我们可以采用零拷贝技术,这个技术就是通过mmap,直接将内核空间的数据通过映射的方法映射到用户空间上,即物理上共用这段数据。
零拷贝介绍https://www.jianshu.com/p/fad3339e3448
默认构造函数、一般构造函数、拷贝构造函数
(1)默认构造函数(无参数):如果创建一个类你没有写任何构造函数,则系统会自动生成默认的构造函数,或者写了一个不带任何形参的构造函数。
(2)一般构造函数:一般构造函数可以有各种参数形式,一个类可以有多个一般构造函数,前提是参数的个数或者类型不同(基于c++的重载函数原理)。
(3)拷贝构造函数参数为类对象本身的引用,用于根据一个已存在的对象复制出一个新的该类的对象,一般在函数中会将已存在对象的数据成员的值复制一份到新创建的对象中。参数(对象的引用)是不可变的(const类型)。此函数经常用在函数调用时用户定义类型的值传递及返回。
(1)用类的一个对象去初始化另一个对象时
(2)当函数的形参是类的对象时(也就是值传递时),如果是引用传递则不会调用
(3)当函数的返回值是类的对象或引用时
(1).前面的地址必须是后面的地址正数倍,不是就补齐
(2).整个Struct的地址必须是最大字节的整数倍
(3)为什么要?
(1)首先说到c++内存泄漏时要知道它的含义?
内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
(2)内存泄漏的后果?
最难捉摸也最难检测到的错误之一是内存泄漏,即未能正确释放以前分配的内存的 bug。 只发生一次的小的内存泄漏可能不会被注意,但泄漏大量内存的程序或泄漏日益增多的程序可能会表现出各种征兆:从性能不良(并且逐渐降低)到内存完全用尽。 更糟的是,泄漏的程序可能会用掉太多内存,以致另一个程序失败,而使用户无从查找问题的真正根源。 此外,即使无害的内存泄漏也可能是其他问题的征兆。
(3)对于C和C++这种没有垃圾回收机制的语言来讲,我们主要关注两种类型的内存泄漏:
堆内存泄漏 (Heap leak)。对内存指的是程序运行中根据需要分配通过malloc,realloc new等从堆中分配的一块内存,再是完成后必须通过调用对应的 free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak.
系统资源泄露(Resource Leak).主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。
(4)使用C/C++语言开发的软件在运行时,出现内存泄漏。可以使用以下两种方式,进行检查排除:
使用工具软件BoundsChecker,BoundsChecker是一个运行时错误检测工具,它主要定位程序运行时期发生的各种错误。
调试运行DEBUG版程序,运用以下技术:CRT(C run-time libraries)、运行时函数调用堆栈、内存泄漏时提示的内存分配序号(集成开发环境OUTPUT窗口),综合分析内存泄漏的原因,排除内存泄漏。
(5)解决内存泄漏最有效的办法就是使用智能指针(Smart Pointer)。
使用智能指针就不用担心这个问题了,因为智能指针可以自动删除分配的内存。智能指针和普通指针类似,只是不需要手动释放指针,而是通过智能指针自己管理内存的释放,这样就不用担心内存泄漏的问题了。
shared_ptr基于“引用计数”模型实现,多个shared_ptr可指向同一个动态对象,并维护了一个共享的引用计数器,记录了引用同一对象的shared_ptr实例的数量。当最后一个指向动态对象的shared_ptr销毁时,会自动销毁其所指对象(通过delete操作符)。
shared_ptr的默认能力是管理动态内存,但支持自定义的Deleter以实现个性化的资源释放动作。
weak_ptr用于解决“引用计数”模型循环依赖问题,weak_ptr指向一个对象,并不增减该对象的引用计数器
C++11中引入了三种智能指针,分别是shared_ptr、weak_ptr和unique_ptr
智能指针的作用
智能指针可以帮助我们管理动态分配的堆内存,减少内存泄漏的可能性
手动管理堆内存有引起内存泄漏的可能,比如这段代码
try {
int* p = new int;
// Do something
delete p;
} catch(...) {
// Catch exception
}
如果在执行Do something的时候发生了异常,那么程序就会直接跳到catch语句捕获异常,delete p这句代码不会被执行,发生了内存泄漏
我们把上面的程序改成
try {
shared_ptr<int> p(new int);
// Do something
} catch(...) {
// Catch exception
}
当执行Do something的时候发生了异常,那么try块中的栈对象都会被析构。因此代码中p的析构函数会被调用,引用计数从1变成0,通过new分配的堆内存被释放,这样就避免了内存泄漏的问题
(1)循环引用问题
虽然智能指针会减少内存泄漏的可能性,但是如果使用智能指针的方式不对,一样会造成内存泄漏。比较典型的情况是循环引用问题,比如这段代码
class B; // 前置声明
class A {
public:
shared_ptr<B> ptr;
};
class B {
public:
shared_ptr<A> ptr;
};
int main()
{
while(true) {
shared_ptr<A> pa(new A());
shared_ptr<B> pb(new B());
pa -> ptr = pb;
pb -> ptr = pa;
}
return 0;
}
这个程序中智能指针的引用情况如下图
上图中,class A和class B的对象各自被两个智能指针管理,也就是A object和B object引用计数都为2,为什么是2?
分析class A对象的引用情况,该对象被main函数中的pa和class B对象中的ptr管理,因此A object引用计数是2,B object同理。
在这种情况下,在main函数中一个while循环结束的时候,pa和pb的析构函数被调用,但是class A对象和class B对象仍然被一个智能指针管理,A object和B object引用计数变成1,于是这两个对象的内存无法被释放,造成内存泄漏,如下图所示
(2)解决方法
解决方法很简单,把class A或者class B中的shared_ptr改成weak_ptr即可,由于weak_ptr不会增加shared_ptr的引用计数,所以A object和B object中有一个的引用计数为1,在pa和pb析构时,会正确地释放掉内存
(3)weak_ptr的使用
使用 weak_ptr 解决 shared_ptr 因循环引有不能释放资源的问题
注意: 虽然通过弱引用指针可以有效的解除循环引用, 但这种方式必须在程序员能预见会出现循环引用的情况下才能使用, 也可以是说这个仅仅是一种编译期的解决方案, 如果程序在运行过程中出现了循环引用, 还是会造成内存泄漏.
内存泄漏的方法很多,可以用gdb打开core文件,确定出错的堆栈地点,从而判断程序出错的位置。
eg:
core dump又叫核心转储。当程序运行过程中发生异常, 程序异常退出时, 由操作系统把程序当前的内存状况存储在一个core文件中, 叫core dump。
(1)ulimit -c unlimited命令设置coredump文件
(2)gdb a.out core命令运行程序(linux下)
(3)使用bt命令查看堆栈
linux可以使用开源的Valgrind工具包,包含多个工具:Memcheck常用语检测malloc和new这类的问题,callgrind用来检查函数调用,cachegrind缓存使用,helgrind多线程程序中的竞争。除了valgrind还可以用mtrace这些工具
模板是C11里面添加的,使用与在不知道类型的情况下,编写一个泛型的程序,模板通过用一个指定的关键字来代替类型,进行泛型编程。
应用场景:应用场景很多,例如我们要编程一些和类型无关的代码时,STL里面的很多容器都是用到了模板,容器的功能都可以使用,但并没有确定容器里面一定要用指定的类型,可以是任何的类型。
成员初始化列表:
但是有时候必须使用带初始化列表的构造函数:
为什么成员初始化列表效率更高?
类对象的构造顺序是这样的:
(1)分配内存,调用构造函数时,隐式/显示的初始化各数据成员;
(2)进入构造函数后在构造函数中执行一般赋值与计算。
类对象的构造顺序显示,进入构造函数体后,进行的是计算,是对成员变量的赋值操作,显然,赋值和初始化是不同的,这样就体现出了效率差异,如果不用成员初始化类表,那么类对自己的类成员分别进行的是一次隐式的默认构造函数的调用,和一次赋值操作符的调用,如果是类对象,这样做效率就得不到保障。
注意:构造函数需要初始化的数据成员,不论是否显示的出现在构造函数的成员初始化列表中,都会在该处完成初始化,并且初始化的顺序和其在类中声明时的顺序是一致的,与列表的先后顺序无关,所以要特别注意,保证两者顺序一致才能真正保证其效率和准确性。
例如:decltype,lambda表达式,智能指针, forward_list,tuple,正则表达式库,随机数
nullptr 专门代表空指针
auto 自动进行类型推导
引入了基于范围的迭代写法for(auto &i : arr)
初始化列表
引入了外部模板,能够显式的告诉编译器何时进行模板的实例化
可以指定模板的默认参数
引入了委托构造的概念,这使得构造函数可以在同一个类中一个构造函数调用另一个构造函数
提供了一个匿名函数的特性
对于程序,编译器会对其分配一段内存,在逻辑上可以分为代码段,数据段,堆,栈
代码段:保存程序文本,指令指针EIP就是指向代码段,可读可执行不可写
数据段:保存初始化的全局变量和静态变量,可读可写不可执行
BSS:未初始化的全局变量和静态变量
堆(Heap):动态分配内存,向地址增大的方向增长,可读可写可执行
栈(Stack):存放局部变量,函数参数,当前状态,函数调用信息等,向地址减小的方向增长,非常非常重要,可读可写可执行
程序开始,从main开始,首先将参数压入栈,然后压入函数返回地址,进行函数调用,通过跳转指定进入函数,将函数内部的变量去堆栈上开辟空间,执行函数功能,执行完成,取回函数返回地址,进行下一个函数。
四种强制转换是static_cast、dynamic_cast、const_cast、reinterpret_cast。
static_cast:静态强制转换,类似传统c语言里面括号的强制转换
dynamic_cast:动态强制转换,主要应用于多态,父子类的类型转换,dynamic_cast和static_cast不同的是,它会检查类型转换是否正确,不能转换,则会返回null,所以不算是强制转换。
const_cast:取出const属性,比较简单,可以把const类型转换为非conse指针类型。
reinterpret_cast:一种非常随意的二进制转换,简单理解对一个二进制序列的重新解释。
对于所调用的函数里面的临时变量,在函数调用过程中是被压到程序进程的栈中的,当函数退出时,临时变量出栈,即临时变量已经被销毁,临时变量占用的内存空间没有被清空,但是已经可以被分配给其他变量了,所以有可能在函数退出时,该内存已经被修改了,对于临时变量来说已经是没有意义的值了。
在C中,16bit程序中,返回值保存在ax寄存器中,32bit程序中,返回值保持在eax寄存器中,如果是64bit返回值,edx寄存器保存高32bit,eax寄存器保存低32bit。
综上,函数是可以将临时变量的值作为返回值的。
但是将一个指向局部变量的指针作为函数的返回值是有问题的。
由于指针指向局部变量,因此在函数返回时,临时变量被销毁,指针指向一块无意义的地址空间,所以一般不会有返回值。
如果得到正常的值,只能是幸运的,因为退出函数的时候,系统只是修改了栈顶的指针,并没有清内存;所以,是有可能正常访问到局部变量的内存的。但因为栈是系统自动管理的,所以该内存可能会可以被分配给其他函数,这样,该内存的内容就会被覆盖,不再是原来的值了。
常规程序中,函数返回的指针(函数指针,数组指针,结构体指针,联合体指针等)通常应该是:
(1)指向静态(static)变量;
(2)指向专门申请分配的(如用malloc)空间;
(3)指向常量区(如指向字符串"hello");
(4)指向全局变量;
(5)指向程序代码区(如指向函数的指针)。
除这5项以外,其它怪技巧不提倡。
函数内的变量,没有关键字static修饰的变量的生命周期只在本函数内,函数结束后变量自动销毁。当返回为指针的时候需要特别注意,因为函数结束后指针所指向的地址依然存在,但是该地址可以被其他程序修改,里面的内容就不确定了,有可能后面的操作会继续用到这块地址,有可能不会用到,所以会出现时对时错的情况,如果需要返回一个指针而又不出错的话只能调用内存申请函数
对于结构体和联合体来说,在作为函数的参数和返回值时,表现与C语言的内置类型(int,float, char等)是一样的,当为临时变量的时候,作为返回值时有效的。这个也是与指针不同的地方,所以一定要区分好,总是为当返回结构体或者联合体的时候,该怎么处理,原来直接返回就可以了…
https://www.cnblogs.com/nbk-zyc/p/12449331.html c++中的异常处理
http://c.biancheng.net/view/422.html C++异常处理(try catch throw)完全攻略
禁止编译器优化,每次从内存中读取数据,有利于线程安全。
volatile用在如下的几个地方:
转载自:http://www.708luo.com/?p=36
冗余的变量拷贝
相对C而言,写C++代码经常一不小心就会引入一些临时变量,比如函数实参、函数返回值。在临时变量之外,也会有其他一些情况会带来一些冗余的变量拷贝。
之前针对冗余的变量拷贝问题写过一些帖子,详情请点击这里。
多重过滤
很多服务都会过滤的部分结果的需求,比如游戏交谈中过滤需要过滤掉敏感词。假设现在有两个过滤词典,一个词典A内容较少,另一个词典B内容较多,现在有1000个词需要验证合法性。
词落在词典A中的概率是1%,落在词典B中的概率是10%,而判断词是否落在词典A或B中的操作耗时差不多,记作N。
那么要判断词是否合法,有两种方式:
先判断词是否在A中,如果在返回非法;如果不在再判断是否在B中,如果在返回非法,否则返回合法。
和方式一类似,不过是先判断是否在B中。
现在我们来计算两种方式的耗时:
1000N+1000(1-1%)*N
1000N+1000(1-10%)*N
很明显,方式二的过滤操作排序优化方式一。
说得有些啰嗦,其实简单点说就是一句话:多重过滤中把强过滤前移;过滤强度差不多时,过滤消耗较小的前移。
如果有些过滤条件较强,但是过滤消耗也较大怎么办?该前移还是后移?个人到没遇到过这种情况,如果确实需要考虑,也可以用之前计算方式一、二整体耗时的方法也计算一遍。
字符数组的初始化
一些情况是:写代码时,很多人为了省事或者说安全起见,每次申请一段内存之后都先全部初始化为0。
另一些情况是:用了一些API,不了解底层实现,把申请的内存全部初始化为0了,比如char buf[1024]=""的方式,有篇帖子写得比较细,请看这里。
上面提到两种内存初始化为0的情况,其实有些时候并不是必须的。比如把char型数组作为string使用的时候只需要初始化第一个元素为0即可,或者把char型数组作为一个buffer使用的大部分时候根本不需要初始化。
频繁的内存申请、释放操作
曾经遇到过一个性能问题是:一个服务在启动了4-5小时之后,性能突然下降。
查看系统状态发现,这时候CPU的sys态比较高,同时又发现系统的minflt值迅速增加,于是怀疑是内存的申请、释放造成的性能下降。
最后定位到是服务的处理线程中,在处理请求时有大量申请和释放内存的操作。定位到原因之后就好办了,直接把临时申请的内存改为线程变量,性能一下子回升了。
能够迅速的怀疑到是临时的内存申请造成的性能下降,还亏之前看过这篇帖子。
至于为什么是4-5小时之后,性能突然下降,则怀疑是内存碎片的问题。
提前计算
这里需要提到的有两类问题:
局部的冗余计算:循环体内的计算提到循环体之前
全局的冗余计算
问题1很简单,大部分人应该都接触到过。有人会问编译器不是对此有对应的优化措施么?对,公共子表达式优化是可以解决一些这个问题。不过实测发现如果循环体内是调用的某个函数,即使这个函数是没有side effect的,编译器也无法针对这种情况进行优化。(我是用gcc 3.4.5测试的,不排除更高版本的gcc或者其他编译器可以针对这种情况进行优化)
对于问题2,我遇到的情况是:服务代码中定义了一个const变量,假设叫做MAX_X,处理请求是,会计算一个pow(MAX_X)用作过滤阈值,而性能分析发现,这个pow操作占了整体系统CPU占用的10%左右。对于这个问题,我的优化方式很简单,直接计算定义一个MAX_X_POW变量用作过滤即可。代码修改2行,性能提升10%。
空间换时间
这其实是老生常谈、在大学里就经常提到的问题了。
不过第一次深有体会的应用却是在前段时间刚遇到。简单来说是这样一个应用场景:系统内有一份词表和一份非法词表,原来的处理逻辑是根据请求中的数据查找到对应的词(很多),然后用非法词表过滤掉其中非法的部分。对系统做性能分析发现,依次判断查找出来的词是否在非法词表中的操作比较耗性能,能占整体系统消耗CPU的15-20%。后来的优化手段其实也不复杂,就是服务启动加载词表和非法词表的时候,再生成一张合法词表,请求再来的时候,直接在合法词表中查到结果即可。不直接用合法词表代替原来那份总的词表的原因是,总的词表还是其他用途。
内联频繁调用的短小函数
很多人知道这个问题,但是有时候会不太关注,个人揣测可能的原因有:
编译器会内联小函数
觉得函数调用的消耗也不是特别大
针对1,我的看法是,即使编译器会内联小函数,如果把函数定义写在cpp文件中并在另外一个cpp中调用该函数,这时编译器无法内联该调用。
针对2,我的实际经验是,内联了一个每个请求调用几百次的get操作之后,响应时间减少5%左右。
位运算代替乘除法
据说如果是常量的运算的话,编译器会自动优化选择最优的计算方式。这里的常量计算不仅仅是指"48"这样的操作,也可能是"ab"但编译的时候编译器已经可以知道a和b的值。
不过在编译阶段无法知道变量值的时候,将*、/、% 2的幂的运算改为位运算,对性能有时还是蛮有帮助的。
我遇到的一次优化经历是,将每个请求都会调用几十到数百次不等的函数中一个*8改为<<3和一个%8改为&7之后,服务器的响应时间减少了5%左右。
下面是我实测的一些数据:
%2的次方可以用位运算代替,a%8=a&7(两倍多效率提升)
/2的次方可以用移位运算代替,a/8=a>>3(两倍多效率提升)
2的次方可以用移位运算代替,a8=a<<3(小数值测试效率不明显,大数值1.5倍效率)
整数次方不要用pow,ii比pow(i,2)快8倍,ii*i比pow快40倍
strncpy, snprintf效率对比:目标串>>源串 strncpy效率低,源串>>目标串 snprintf效率低
编译优化
gcc编译的时候,很多服务都是采用O2的优化选项了。不过在使用公共库的时候,可能没注意到就使用了一个没开任何优化的产出了。我就遇到过至少3个服务因为打开了tcmalloc库的O2选项之后性能提升有10%以上的。
不过开O2优化,有些时候可能会遇到一些非预期的结果,比如这篇帖子提到的memory aliasing的问题。
(1) 访问权限
(2)继承关系的访问控制
auto
1.编译器通过分析表达式的类型来确定变量的类型,所以auto定义的变量必须有初始值。
auto i=10; //ok,i为整型
auto j; //error,定义时必须初始化。
j=2;
2.auto可以在一条语句中声明多个变量,但该语句中所有变量的初始值类型必须有一样。
auto i=0,*P=&i; //ok,i是整数,p是整型指针
auto a=2,b=3.14; //error,a和b类型不一致
3.auto会忽略掉顶层const,同时底层const则会保留下来
const int a=2,&b=a;
auto c=a; //c是int 型,而不是const int,即忽略了顶层const
auto d=&a; //d是一个指向const int 的指针,即保留了底层const
如果希望auto类型是一个顶层const ,需要明确指出:
const auto e=a; //e是const int 类型
4.当使用数组作为auto变量的初始值时,推断得到的变量类型是指针,而非数组
int a[10]={1,2,3,4,5,6,7,8,9,0}
auto b=a; //b是int *类型,指向数组的第一个元素
int c[2][3]={1}
auto d=c; //d是int(*d)[3]类型的数组指针
for(auto e:c) //e是int*类型,而不是int(*)[3]
for(auto &f:c) //f是int(&f)[3]
//**************************************************
decltype (a) c; //c是由10个整型数构成的数组,c[10]
decltype
decltype和auto功能类型,但略有区别:
1.decltype根据表达式类型确定变量类型,但不要求定义时进行初始化
int a=2;
decltype (a) b; //b是int类型
b=3;
int &c=a;
decltype (c) d=a; //d为int &类型,因此定义时必须初始化
2.解引用指针操作将得到引用类型
int a=2,*b=a;
decltype (*b) c=a; //解引用,c是int &类型,因此必须初始化
3.decltype所用的表达式加()得到的是该类型的引用
int a=2;
decltype ((a)) b=a; //b是int&类型,而不是int类型,必须初始化
decltype (a) c; //c是int类型
4.decltype所用变量时数组时,得到的同类型的数组,而不是指针
int a[2]={1,2}
decltype (a) b={3,4} //int b[2]类型
5.decltype所用变量是函数时,得到的是函数类型,而不是函数指针
int fun(int a);
decltype(fun) *f(); //函数f返回的是 int(*)(int),即函数指针,而decltype(fun)是int(int)类型
(1)内联函数在编译时展开,宏在预编译时展开;
(2)内联函数直接嵌入到目标代码中,宏是简单的做文本替换;
(3)内联函数有类型检测、语法判断等功能,而宏没有;
(4)inline函数是函数,宏不是;
(5)宏定义时要注意书写(参数要括起来)否则容易出现歧义,内联函数不会产生歧义;
类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图访问自己没被授权的内存区域。绝对类型安全的编程语言暂时还没有。
C语言的类型安全
C只在局部上下文中表现出类型安全,比如试图从一种结构体的指针转换成另一种结构体的指针时,编译器将会报告错误,除非使用显式类型转换。然而,C中相当多的操作是不安全的。
如果C++使用得当,它将远比C更有类型安全性。相比于C,C++提供了一些新的机制保障类型安全:
(1)操作符new返回的指针类型严格与对象匹配,而不是void *;
(2)C中很多以void*为参数的函数可以改写为C++模板函数,而模板是支持类型检查的;
(3)引入const关键字代替#define constants,它是有类型、有作用域的,而#define constants只是简单的文本替换;
(4)一些#define宏可被改写为inline函数,结合函数的重载,可在类型安全的前提下支持多种类型,当然改写为模板也能保证类型安全;
(5)C++提供了dynamic_cast关键字,使得转换过程更加安全,因为dynamic_cast比static_cast涉及更多具体的类型检查。即便如此,C++也不是绝对类型安全的编程语言。如果使用不得当,同样无法保证类型安全。
https://blog.csdn.net/ask233/article/details/99713381
https://liyiye012.github.io/2018/09/21/C++%E9%9D%A2%E8%AF%95%E9%AB%98%E9%A2%91%E9%A2%98/
https://blog.csdn.net/weixin_43819197/article/details/94407751