C++后台开发知识总结(一)C++基础

相关:
C++后台开发知识总结(一)C++基础
C++后台开发知识总结(二)数据库
C++后台开发知识总结(三)计算机网络
C++后台开发知识总结(四)操作系统/Linux 内核

extern “C”的作用

extern “C”的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern “C”后,会指示编译器这部分代码按C语言(而不是C++)的方式进行编译

原因是:C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;
而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。

1
2
3
4
5
6
7
8
9
10
#ifndef __INCvxWorksh /*防止该头文件被重复引用*/
#define __INCvxWorksh
#ifdef __cplusplus //告诉编译器,这部分代码按C语言的格式进行编译,而不是C++的
extern "C"{
#endif
/*…*/
#ifdef __cplusplus
}
#endif
#endif /*end of __INCvxWorksh*/

C++和C的区别

设计思想上:
C++是面向对象的语言,而C是面向过程的结构化编程语言
语法上:
C++具有封装、继承和多态三种特性
C++相比C,增加多许多类型安全的功能,比如强制类型转换
C++支持范式编程,比如模板类、函数模板等

C/C++ const关键字

(1)欲阻止一个变量被改变,可以使用const关键字。在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;
(2)对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;
(3)在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;可以阻止用户修改返回值。返回值也要相应的付给一个常量或常指针。
(4)对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量。

C/C++ static关键字

1.全局静态变量
在全局变量前加上关键字static,全局变量就定义成一个全局静态变量.
静态存储区,在整个程序运行期间一直存在。
初始化:未经初始化的全局静态变量会被自动初始化为0
作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾。
2.局部静态变量
在局部变量之前加上关键字static,局部变量就成为一个局部静态变量。
内存中的位置:静态存储区
初始化:未经初始化的全局静态变量会被自动初始化为0
作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变;

1
2
3
4
int fun(){
static int count = 10; //在第一次进入这个函数的时候,变量a被初始化为10!并接着自减1
return count--; //以后每次进入该函数,a就不会被再次初始化了,仅进行自减1的操作
}

3.静态函数
在函数返回类型前加static,函数就定义为静态函数。函数的定义和声明在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。
函数的实现使用static修饰,那么这个函数只可在本cpp内使用,不会同其他cpp中的同名函数引起冲突; warning:不要再头文件中声明static的全局函数,不要在cpp内声明非static的全局函数,如果你要在多个cpp中复用该函数,就把它的声明提到头文件里去,否则cpp内部声明需加上static修饰;
4.类的静态成员及静态成员函数
对象与对象之间的成员变量是相互独立的。要想共用数据,则需要使用静态成员和静态方法。
只要在类中声明静态成员变量,即使不定义对象,也可以为静态成员变量分配空间,进而可以使用静态成员变量。
静态成员变量是在程序编译时分配空间,而在程序结束时释放空间。
初始化静态成员变量要在类的外面进行。不能用参数初始化表,对静态成员变量进行初始化。

在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员(这点非常重要)。
可以通过类名/对象名直接访问类的公有静态成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class test
{
private:
static int m_value; //定义私有类的静态成员变量

public:
static int getValue() //定义类的静态成员函数
{
return m_value;
}
};
int test::m_value = 0; //类的静态成员变量需要在类外分配内存空间

int main()
{
test t1;
test t2;
test t3;

//通过类名直接调用公有静态成员函数,获取对象个数
cout << "test::m_value2 = " << test::getValue() << endl;
//通过对象名调用静态成员函数获取对象个数
cout << "t3.getValue() = " << t3.getValue() << endl;
system("pause");
}

C/C++ volatile关键字

volatile是“易变的”、“不稳定”的意思。volatile是C的一个较为少用的关键字,它用来解决变量在“共享”环境下容易出现读取错误的问题
变量可能会被意想不到地改变,即在你程序运行过程中一直会变,你希望这个值被正确的处理,凡是申明为volatile的变量,每次都是从内存中读取变量的值,而不是在某些情况下直接从寄存器中取值。(有可能被其他的程序(如中断程序、另外的线程等)所修改)

应用场景:
(1)并行设备的硬件寄存器(如状态寄存器)反复读操作,编译器在优化后,也许读操作只做了一次
(2)一个中断服务子程序中访问到的变量
(3)多线程应用中被多个任务共享的变量
当多个线程共享某一个变量时,该变量的值会被某一个线程更改,应该用 volatile 声明。作用是防止编译器优化把变量从内存装入CPU寄存器中,当一个线程更改变量后,未及时同步到其它线程中导致程序出错。

(1)一个参数既可以是const还可以是volatile吗?为什么?
是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
(2)一个指针可以是volatile吗?为什么?
是的。尽管这并不很常见。一个例子是当一个中断服务子程序修该一个指向一个buffer的指针时
(3)下面的函数有什么错误?

1
2
3
4
5
6
7
8
9
10
11
int square(volatile int *ptr) 
{
return *ptr * *ptr;
}
//正确的代码如下:
long square(volatile int *ptr)
{
int a;
a = *ptr;
return a * a;
}

volatile能够避免编译器优化带来的错误,但使用volatile的同时,也需要注意频繁地使用volatile很可能会增加代码尺寸和降低性能,因此要合理的使用volatile。

C/C++ restrict关键字

1
2
const int a=10;
int * b=&a; //警告:初始化丢弃了指针目标类型的限定

b失去了对目标对象的const的限定,并且可以通过指针b更改它们共同指向的空间。
const是无法保证某个对象不被更改的,restrict关键字是修饰指针的,对该指针指向的空间的访问,只能从这个指针进入。

什么是右值引用,跟左值又有什么区别?

主要目的有两个方面:
1.消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
2.能够更简洁明确地定义泛型函数。
左值:能对表达式取地址、或具名对象/变量。一般指表达式结束后依然存在的持久对象。
右值:不能对表达式取地址,或匿名对象。一般指表达式结束就不再存在的临时对象。 右值引用和左值引用的区别:
(1)左值可以寻址,而右值不可以。
(2)左值可以被赋值,右值不可以被赋值,可以用来给左值赋值。
(3)左值可变,右值不可变(仅对基础类型适用,用户自定义类型右值引用可以通过成员函数改变)。

1
int i = getVar();

从getVar()函数获取一个整形值,然而,这行代码会产生两种类型的值,一种是左值i,一种是函数getVar()返回的临时值,这个临时值在表达式结束后就销毁了,而左值i在表达式结束后仍然存在,这个临时值就是右值。区分左值和右值的一个简单办法是:看能不能对表达式取地址,如果能,则为左值,否则为右值。

1
int i = 0;

i 是左值,0 是字面量,就是右值。在上面的代码中,i 可以被引用,0 就不可以了。

1
int&& k = getVar();

对右值的引用就是右值引用,getVar()产生的临时值不会像第一行代码那样,在表达式结束之后就销毁了,而是会被“续命”,他的生命周期将会通过右值引用得以延续,和变量k的声明周期一样长。

C++11有哪些新特性

1.auto关键字
编译器可以根据初始值自动推导出类型。但是不能用于函数传参以及数组类型的推导
2.nullptr关键字
nullptr是一种特殊类型的字面值,它可以被转换成任意其它的指针类型;而NULL一般被宏定义为0,在遇到重载时可能会出现问题。
3.智能指针
C++11新增了std::shared_ptr、std::weak_ptr等类型的智能指针,用于解决内存管理的问题。
4.初始化列表
使用初始化列表来对类进行初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Line
{
public:
double getLength( void );
Line(double len);

private:
double length;
};
// 成员函数定义,包括构造函数
Line::Line( double len)
{
cout << "Object is being created, length = " << len << endl;
length = len;
}
// 等同于
Line::Line( double len): length(len)
{
cout << "Object is being created, length = " << len << endl;
}

5.右值引用
基于右值引用可以实现移动语义和完美转发,消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率
6.lambda
Lambda表达式定义一个匿名函数,并且可以捕获一定范围内的变量
[捕获列表] (参数列表) mutable或exception声明 ->返回值类型 {函数体}
[捕获列表],捕获上下文变量以供lambda使用。标识一个Lambda的开始,这部分必须存在,不能省略。
(参数列表),与普通函数的参数列表一致,如果不需要传递参数,则可以连通括号一起省略。参数可以通过按值(如:(a,b))和按引用(如:(&a,&b))两种方式进行传递。
mutable是修饰符,默认情况下lambda函数总是一个const函数,Mutable可以取消其常量性。在使用该修饰符时,参数列表不可省略。
->返回值类型, 当返回值为void,或者函数体中只有一处return的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略。
{函数体},内容与普通函数一样,除了可以使用参数之外,还可以使用所捕获的变量。
Lambda表达式与普通函数最大的区别就是其可以通过捕获列表访问一些上下文中的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
vector<int> vctTemp;
vctTemp.push_back(1);
vctTemp.push_back(2);

{ // 无函数对象参数,输出:1 2
for_each(vctTemp.begin(), vctTemp.end(), [](int v){ cout << v << endl; });
}

{ // 以值方式传递作用域内所有可见的局部变量(包括this),输出:11 12
int a = 10;
for_each(vctTemp.begin(), vctTemp.end(), [=](int v){ cout << v+a << endl; });
}

{ // 以引用方式传递作用域内所有可见的局部变量(包括this),输出:11 13 12
int a = 10;
for_each(vctTemp.begin(), vctTemp.end(), [&](int v)mutable{ cout << v+a << endl; a++; });
cout << a << endl;
}

{ // 以值方式传递局部变量a,输出:11 13 10
int a = 10;
for_each(vctTemp.begin(), vctTemp.end(), [a](int v)mutable{ cout << v+a << endl; a++; });
cout << a << endl;
}

{ // 以引用方式传递局部变量a,输出:11 13 12
int a = 10;
for_each(vctTemp.begin(), vctTemp.end(), [&a](int v){ cout << v+a << endl; a++; });
cout << a << endl;
}

{ // 传递this,输出:21 22
for_each(vctTemp.begin(), vctTemp.end(), [this](int v){ cout << v+m_nData << endl; });
}

{ // 除b按引用传递外,其他均按值传递,输出:11 12 17
int a = 10;
int b = 15;
for_each(vctTemp.begin(), vctTemp.end(), [=, &b](int v){ cout << v+a << endl; b++; });
cout << b << endl;
}

{ // 操作符重载函数参数按引用传递,输出:2 3
for_each(vctTemp.begin(), vctTemp.end(), [](int &v){ v++; });
for_each(vctTemp.begin(), vctTemp.end(), [](int v){ cout << v << endl; });
}

{ // 空的Lambda表达式
[](){}();
[]{}();
}

7.可变参数模板
C++11的可变参数模板,对参数进行了高度泛化,可以表示任意数目、任意类型的参数,其语法为:在class或typename后面带上省略号。
通过递归函数展开参数包,需要提供一个参数包展开的函数和一个递归终止函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//递归终止函数
template<typename T>
void myprint(T end)//递归到最后一次,调用单参数函数
{
cout << "parameter " << end << endl;
}

//展开函数
template<typename T, class ...Args>
void myprint(T head, Args... rest)
{
cout << "parameter " << head << endl;
myprint(rest...);
}

int main()
{
myprint(1, 2, 3, 4);
return 0;
}

8.atomic原子操作用于多线程资源互斥操作
9.新增STL容器array以及tuple

C++智能指针

智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。 C++ 11中最常用的智能指针类型为shared_ptr,它采用引用计数的方法,记录当前内存资源被多少个智能指针引用。该引用计数的内存在堆上分配。当新增一个时引用计数加1,当过期时引用计数减一。只有引用计数为0时,智能指针才会自动释放引用的内存资源。
对shared_ptr进行初始化时不能将一个普通指针直接赋值给智能指针,因为一个是指针,一个是类。可以通过make_shared函数或者通过构造函数传入普通指针。并可以通过get函数获得普通指针。
C++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11支持,并且第一个已经被11弃用。#include<memory>
1.unique_ptr(替换auto_ptr)
保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用。

1
2
3
unique_ptr<string> p3 (new string ("auto"));   //#4
unique_ptr<string> p4; //#5
p4 = p3;//此时会报错!!

编译器认为p4=p3非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全。
当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做,比如:

1
2
3
4
5
unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1; // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You")); // #2 allowed

其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。
注:如果确实想执行类似与#1的操作,要安全的重用这种指针,可给它赋新值。C++有一个标准库函数std::move(),能够将一个unique_ptr赋给另一个。

  1. shared_ptr
    多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。
    shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。
    成员函数:

use_count 返回引用计数的个数
unique 返回是否是独占所有权( use_count 为 1)
swap 交换两个 shared_ptr 对象(即交换所拥有的对象)
reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少
get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptr sp(new int(1)); sp 与 sp.get()是等价的

  1. weak_ptr
    weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr.
    weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    class B;
    class A
    {
    public:
    shared_ptr<B> pb_;
    ~A(){
    cout<<"A delete\n";
    }
    };
    class B
    {
    public:
    shared_ptr<A> pa_;
    ~B(){
    cout<<"B delete\n";
    }
    };
    void fun()
    {
    shared_ptr<B> pb(new B());
    shared_ptr<A> pa(new A());
    pb->pa_ = pa;
    pa->pb_ = pb;
    cout<<pb.use_count()<<endl; //2
    cout<<pa.use_count()<<endl; //2
    }
    int main()
    {
    fun();
    return 0;
    }

可以看到fun函数中pa ,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减一,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A B的析构函数没有被调用),如果把其中一个改为weak_ptr就可以了,我们把类A里面的shared_ptr pb_; 改为weak_ptr pb_; 运行结果如下在这里插入图片描述 ,这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减一,同时pa析构时使A的计数减一,那么A的计数为0,A得到释放。
注意的是我们不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问,pa->pb_->print(); 因为pb_是一个weak_ptr,应该先把它转化为shared_ptr,如:

1
2
shared_ptr p = pa->pb_.lock();
p->print();

隐式类型转换

隐式转换指的是不需要用户干预,编译器私下进行的类型转换行为。
首先,对于内置类型,低精度的变量给高精度变量赋值会发生隐式类型转换。如: int 到 double

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//混合类型的算术运算表达式中
int a = 3;
double b = 4.5;
double c = a + b; // a 将被自动转换为 double 类型,再加法。
//不同类型的赋值操作中//
int a = true; // bool 类型转换为 int 类型
int *ptr = null; // null 被转换为 int * 类型
//函数参数传值
void func(double a) {};
func(1); // 1 被隐式的转换为 double 类型1.0
//函数返回值
double add(int a, int b)
{
return a + b; // 运算的结果会被隐式的转换为 double 类型再返回
}

其次,对于只存在单个参数的构造函数的对象构造来说,函数调用可以直接使用该参数传入,编译器会自动调用其构造函数生成临时对象。

1
2
3
4
BOOK A("A-A-A");
BOOK B("B-B-B");
cout<<A.isSameISBN(B)<<endl; //正经地进行比较,无需发生转换
cout<<A.isSameISBN(string("A-A-A"))<<endl; //此处即发生一个隐式转换:string类型-->BOOK类型,借助BOOK的构造函数进行转换,以满足isSameISBN函数的参数期待。

禁止隐式转换:explicit,该关键字只能用来修饰类内部的构造函数;

1
explicit BOOK(string ISBN,float price=0.0f):_bookISBN(ISBN),_price(price){}

C++四种类型转换

C风格的强制类型转换很简单,均用 Type b = (Type)a 形式转换。
C++风格的类型转换提供了4种类型转换操作符来应对不同场合的应用
1、const_cast 去掉类型的const或volatile属性
去掉类型的const或volatile属性

1
2
3
4
5
6
7
8
struct T {
int i;
};

const T a;
//a.i = 10; //直接修改const类型,编译错误
T &b = const_cast<T&>(a);
b.i = 10;

2、static_cast 无条件转换,静态类型转换
基类和子类之间的转换:其中子类指针转换为父类指针是安全的,但父类指针转换为子类指针是不安全的(基类和子类之间的动态类型转换建议用dynamic_cast)。
基本数据类型转换,enum,struct,int,char,float等。static_cast不能进行无关类型(如非基类和子类)指针之间的转换。
把任何类型的表达式转换成void类型。
3、dynamic_cast 有条件转换,动态类型转换,运行时检查类型安全
更多使用static_cast,dynamic本身只能用于存在虚函数的父子关系的强制类型转换,对于指针,转换失败则返回nullptr,对于引用,转换失败会抛出异常
4、reinterpret_cast 仅重新解释类型,但没有进行二进制的转换
可以用于任意类型的指针之间的转换,对转换的结果不做任何保证
为什么不使用C的强制转换?
C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。

C++源文件从文本到可执行文件经历的过程

对于C++源文件,从文本到可执行文件一般需要四个过程:
预处理阶段:主要处理源代码文件中的以“#”开头的预编译指令。对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件。
编译阶段:将经过预处理后的预编译文件转换成特定汇编代码,生成汇编文件
汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件
链接阶段:将多个目标文件及所需要的库连接成最终的可执行目标文件

静态链接和动态链接

1、静态链接:
函数和数据被编译进一个二进制文件。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件。
空间浪费:因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,会出现同一个目标文件都在内存存在多个副本;
更新困难:每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。
运行速度快:但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。 2、动态链接:
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
共享库:就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多分,副本,而是这多个程序在执行时共享同一份副本;
更新方便:更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。
性能损耗:因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。

include头文件的顺序以及双引号””和尖括号<>的区别

Include头文件的顺序:对于include的头文件来说,如果在文件a.h中声明一个在文件b.h中定义的变量,而不引用b.h。那么要在a.c文件中引用b.h文件,并且要先引用b.h,后引用a.h,否则汇报变量类型未声明错误。
双引号和尖括号的区别:编译器预处理阶段查找头文件的路径不一样。
对于使用双引号包含的头文件,查找头文件路径的顺序为:
当前头文件目录 -> 编译器设置的头文件路径(编译器可使用-I显式指定搜索路径 -> 系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径
对于使用尖括号包含的头文件,查找头文件的路径顺序为:
编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)-> 系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径

什么时候会发生段错误

段错误通常发生在访问非法内存地址的时候,具体来说分为以下几种情况:
使用野指针(指向一个不存在的对象或者未申请访问受限内存区域的指针)
试图修改字符串常量的内容

C++ 模板

模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码。
模板是创建泛型类或函数的蓝图或公式。库容器,比如迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。每个容器都有一个单一的定义,比如 向量,我们可以定义许多不同类型的向量,比如 vector <int> 或 vector <string>
函数模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <string>

using namespace std;

template <typename T>
inline T const& Max (T const& a, T const& b)
{
return a < b ? b:a;
}
int main ()
{
int i = 39;
int j = 20;
cout << "Max(i, j): " << Max(i, j) << endl; //Max(i, j): 39

double f1 = 13.5;
double f2 = 20.7;
cout << "Max(f1, f2): " << Max(f1, f2) << endl; //Max(f1, f2): 20.7

string s1 = "Hello";
string s2 = "World";
cout << "Max(s1, s2): " << Max(s1, s2) << endl; //Max(s1, s2): World

return 0;
}

类模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <iostream>
#include <vector>
#include <cstdlib>
#include <string>
#include <stdexcept>

using namespace std;

template <class T>
class Stack {
private:
vector<T> elems; // 元素

public:
void push(T const&); // 入栈
void pop(); // 出栈
T top() const; // 返回栈顶元素
bool empty() const{ // 如果为空则返回真
return elems.empty();
}
};

template <class T>
void Stack<T>::push (T const& elem)
{
// 追加传入元素的副本
elems.push_back(elem);
}

template <class T>
void Stack<T>::pop ()
{
if (elems.empty()) {
throw out_of_range("Stack<>::pop(): empty stack");
}
// 删除最后一个元素
elems.pop_back();
}

template <class T>
T Stack<T>::top () const
{
if (elems.empty()) {
throw out_of_range("Stack<>::top(): empty stack");
}
// 返回最后一个元素的副本
return elems.back();
}

int main()
{
try {
Stack<int> intStack; // int 类型的栈
Stack<string> stringStack; // string 类型的栈

// 操作 int 类型的栈
intStack.push(7);
cout << intStack.top() <<endl; //7

// 操作 string 类型的栈
stringStack.push("hello");
cout << stringStack.top() << std::endl; //hello
stringStack.pop();
stringStack.pop(); Exception: Stack<>::pop(): empty stack
}
catch (exception const& ex) {
cerr << "Exception: " << ex.what() <<endl;
return -1;
}
}

C++ string实现原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <iostream>
#include <assert.h>
using namespace std;

class String
{
public:
String(const char *str = NULL); // 普通构造函数
String(const String &other); // 拷贝构造函数
~ String(void); // 析构函数
String & operator =(const String &other); // 赋值函数
private:
char *m_data; // 用于保存字符串
};

//普通构造函数
String::String(const char *str)
{
if(str==NULL)
{
m_data = new char[1]; // 得分点:对空字符串自动申请存放结束标志'\0'的空
//加分点:对m_data加NULL 判断
*m_data = '\0';
}
else
{
int length = strlen(str);
m_data = new char[length+1];
strcpy(m_data, str);
}
}
// String的析构函数
String::~String(void)
{
delete [] m_data; // 或delete m_data;
}
//拷贝构造函数
String::String(const String &other)    // 得分点:输入参数为const型
{
int length = strlen(other.m_data);
m_data = new char[length+1];     
strcpy(m_data, other.m_data);
}
//赋值函数
String & String::operator =(const String &other) // 得分点:输入参数为const型
{
if(this == &other)   //得分点:检查自赋值
return *this;
delete [] m_data;     //得分点:释放原有的内存资源
int length = strlen( other.m_data );
m_data = new char[length+1];  
strcpy( m_data, other.m_data );
return *this;         //得分点:返回本对象的引用
}

int main(char argc, char *argv)
{
String s1;
String s2("222");
s1 = s2;

return 0;
}

C++ map、set实现原理:红黑树

1、红黑树:
红黑树是一种二叉查找树,但在每个节点增加一个存储位表示节点的颜色,可以是红或黑(非红即黑)。通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树保证最长路径不超过最短路径的二倍,因而近似平衡。红黑树是一种弱平衡二叉树,相对于要求严格的AVL树来说,它的旋转次数少,所以对于搜索,插入,删除操作较多的情况下,通常使用红黑树。性质:
(1)每个节点非红即黑
(2)根节点是黑的;
(3)每个叶节点(叶节点即树尾端NULL指针或NULL节点)都是的;
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)对于任意节点而言,其到叶子点树NULL指针的每条路径都包含相同数目的黑节点; 2、平衡二叉树(AVL树):
红黑树是在AVL树的基础上提出来的。平衡二叉树又称为AVL树,是一种特殊的二叉排序树。其左右子树都是平衡二叉树,且左右子树高度之差不超过13、红黑树较AVL树的优点:
AVL 树是高度平衡的,频繁的插入和删除,会引起频繁的rebalance,导致效率下降;红黑树不是高度平衡的,算是一种折中,插入最多两次旋转,删除最多三次旋转。 所以红黑树在查找,插入删除的性能都是O(logn),且性能稳定,所以STL里面很多结构包括set、map底层实现都是使用的红黑树。
4、红黑树旋转:
旋转:红黑树的旋转是一种能保持二叉搜索树性质的搜索树局部操作。有左旋和右旋两种旋转,通过改变树中某些结点的颜色以及指针结构来保持对红黑树进行插入和删除操作后的红黑性质。
左旋:对某个结点x做左旋操作时,假设其右孩子为y:以x到y的链为“支轴”进行。使y成为该子树新的根结点,x成为y的左孩子,y的左孩子成为x的右孩子。
在这里插入图片描述
右旋:对某个结点x做右旋操作时,假设其左孩子为y而不是T.nil:以x到y的链为“支轴”进行。使y成为该子树新的根结点,x成为y的右孩子,y的右孩子成为x的左孩子。
在这里插入图片描述

哈夫曼编码

JPEG中就应用了哈夫曼编码。哈夫曼编码是哈夫曼树的一种应用,广泛用于数据文件压缩。
哈夫曼树又称最优二叉树,是一种带权路径长度最短的二叉树。所谓树的带权路径长度,就是树中所有的叶结点的权值乘上其到根结点的路径长度(若根结点为0层,叶结点到根结点的路径长度为叶结点的层数)。树的带权路径长度记为WPL= (W1L1 + W2L2 + W3L3 + … + WnLn),N个权值Wi(i=1,2,…n)构成一棵有N个叶结点的二叉树,相应的叶结点的路径长度为Li(i=1,2,…n)。可以证明哈夫曼树的WPL是最小的。
哈夫曼编码步骤:
一、对给定的n个权值{W1,W2,W3,…,Wi,…,Wn}构成n棵二叉树的初始集合F= {T1,T2,T3,…,Ti,…,Tn},其中每棵二叉树Ti中只有一个权值为Wi的根结点,它的左右子树均为空。(为方便在计算机上实现算 法,一般还要求以Ti的权值Wi的升序排列。)
二、在F中选取两棵根结点权值最小的树作为新构造的二叉树的左右子树,新二叉树的根结点的权值为其左右子树的根结点的权值之和。
三、从F中删除这两棵树,并把这棵新的二叉树同样以升序排列加入到集合F中。
四、重复二和三两步,直到集合F中只有一棵二叉树为止。
哈夫曼编码是一种无前缀编码。解码时不会混淆。其主要应用在数据压缩,加密解密等场合。
a b c d e :5 4 3 2 1
在这里插入图片描述

vector、list、deque、set、map的应用场景

1、set和map
共同点:都是C++的关联容器,只是通过它提供的接口对里面的元素进行访问,底层都是采用红黑树实现
不同点:
set:用来判断某一个元素是不是在一个组里面,使用的比较少;
map:映射,相当于字典,把一个值映射成另一个值,可以创建字典
优点:查找某一个数的时间为O(logn);遍历时采用iterator,效果不错
缺点:每次插入值的时候,都需要调整红黑树,效率有一定影响
2、vector
动态数组,在堆中分配内存,元素连续存放,有保留内存,如果减少大小后,内存也不会释放;如果新值大于当前大小时才会重新分配内存。
特点:拥有一段连续的内存空间,并且起始地址不变,因此能够非常好的支持随机存取,即[]操作符;
对头部和中间进行添加删除元素操作需要移动内存,如果元素是结构或类,那么移动的同时还会进行构造和析构操作,所以性能不高;
对任何元素的访问时间都是O(1),所以常用来保存需要经常进行随机访问的内容,并且不需要经常对中间元素进行添加删除操作
3、list
双向链表,元素也存放在堆中,每个元素都是放在一块内存中,他的内存空间可以是不连续的,通过指针来进行数据的访问,这个特点使得它的随机存取变得非常没有效率,因此它没有提供[]操作符的重载。但是由于链表的特点,它可以很有效率的支持任意地方的删除和插入操作。
特点
在哪里添加删除元素性能都很高,不需要移动内存,当然也不需要对每个元素都进行构造与析构了,所以常用来做随机插入和删除操作容器;
访问开始和最后两个元素最快,其他元素的访问时间都是O(n)
4、deque
分段连续线性空间,支持[]操作符,也就是支持随机存取,有比较高的随机存取速度,由于deque需要处理内部跳转,因此速度上没有vector快

deque vector
组织方式 按页或块来分配存储器的,每页包含固定数目的元素 分配一段连续的内存来存储内容
效率 即使在容器的前端也可以提供常数时间的insert和erase操作,而且在体积增长方面也比vector更具有效率 只是在序列的尾端插入元素时才有效率,但是随机访问速度要比deque快

总结vector list deque
| |vector|list|deque|
|–|–|–|–|
|特点|快速的随机存取,快速的在最后插入删除元素|可以快速的在任意位置添加删除元素,只能快速的访问最开始和最后面的元素|在开始和最后添加删除元素一样快,并且提供了随机访问的方法|
|适用|需要高效的随机存取,不在于插入删除的效率|需要大量的插入和删除操作,不关心随机存取|需要随机存取,也需要高效的在两端进行插入删除操作

vector和list的区别

1)vector底层实现是数组;list是双向链表。
2)vector是顺序内存,支持随机访问,list不行。
4)vector在中间节点进行插入删除会导致内存拷贝,list不会。
5)vector一次性分配好内存,不够时才进行2倍扩容;list每次插入新节点都会进行内存申请。
6)vector随机访问性能好,插入删除性能差;list随机访问性能差,插入删除性能好。

vector、list、map、set迭代器失效

1.对于序列容器vector,deque来说,使用erase(itertor)后,后边的每个元素的迭代器都会失效,但是后边每个元素都会往前移动一个位置,但是erase会返回下一个有效的迭代器; iter =cont.erase(iter)
2.对于关联容器map set来说,使用了erase(iterator)后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素的,不会影响到下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可;
erase(iter++)
3.对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的iterator,因此上面两种正确的方法都可以使用。
| |vector|deque|list|set、map|
|–|–|–|–|–|
|内部数据结构|数组(一段连续内存空间)|数组(多段连续内存空间)|双向环状链表|红黑树|
|插入操作|插入后元素总数不大于capacity,插入位置之后的迭代器会失效;大于capacity,所有迭代器都会失效|两端插入, 不会引起迭代器失效;中间插入, 所有迭代器失效|不会出现迭代器失效|不会出现迭代器失效|
|删除操作|删除位置之后的迭代器都会失效,但是erase会返回下一个有效的迭代器|两端删除, 被删除元素的迭代器失效中间删除, 所有迭代器失效|被删除节点的迭代器失效|被删除节点的迭代器失效
|解决方法| iter =cont.erase(iter)| |iter =cont.erase(iter)或者m.erase(iter++)|m.erase(iter++)

解决方式1: iter =cont.erase(iter)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//要求只是删除值为偶数的节点 , vector<int> vec{ 1, 2, 3, 4, 5 };
vector<int> ::iterator it1 = vec.begin();

//错误实现:链表删除节点导致的迭代器失效,容易造成访问野指针的问题,导致程序崩溃
while (it1 != vec.end()) {
if (*it1 % 2 == 0){
vec.erase(it1);
}
it1++;
}

//正确实现:利用erase()函数的返回值为iterator,返回的是删除节点的下一个位置的迭代器
while (it1 != vec.end()) {
if (*it1 % 2 == 0){
it1 = vec.erase(it1); //重点!!!
}
else{
it1++;
}
}

解决方式2:erase(iter++)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//要求删除键值为偶数的键值对
map<int, int>::iterator it = m.begin();

//错误实现:
while (it != m.end()){
if(it->second % 2 == 0){
m.erase(it);
}
it++;
}

//正确实现:
while (it != m.end()){
if(it->second % 2 == 0){
m.erase(it++); //重点!!!
}
else{
it++;
}
}

C++内存分配管理

在这里插入图片描述
在C++中,虚拟内存分为text代码段、data数据段、bss段、heap堆区、文件映射区以及stack栈区六部分。3G用户空间和1G内核空间
代码段 包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
数据段 存储程序中已初始化的全局变量和静态变量
bss 段 存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。
堆区 调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。
映射区 存储动态链接库以及调用mmap函数进行的文件映射
使用栈空间存储函数的返回地址、参数、局部变量、返回值

内存分配方式有三种:
[1]从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
[2]在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
[3]从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由程序员决定,使用非常灵活,但如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏,频繁地分配和释放不同大小的堆空间将会产生堆内碎块。

A* a = new A; a->i = 10;在内核中的内存分配上发生了什么?

1)A *a:a是一个局部变量,类型为指针,故而操作系统在程序栈区开辟4/8字节的空间(0x000m),分配给指针a。
2)new A:通过new动态的在堆区申请类A大小的空间(0x000n)。
3)a = new A:将指针a的内存区域填入栈中类A申请到的地址的地址。即*(0x000m)=0x000n。
4)a->i:先找到指针a的地址0x000m,通过a的值0x000n和i在类a中偏移offset,得到a->i的地址0x000n + offset,进行*(0x000n + offset) = 10的赋值操作,即内存0x000n + offset的值是10。

给你一个类,里面有static,virtual,之类的,来说一说这个类的内存分布

1、static修饰符
1)static修饰成员变量
无论类被定义了多少个,静态数据成员都只有一份拷贝,为该类型的所有对象所共享(包括其派生类)。所以,静态数据成员的值对每个对象都是一样的,它的值可以更新。
因为静态数据成员在全局数据区分配内存,属于本类的所有对象共享,所以它不属于特定的类对象,在没有产生类对象前就可以使用。
2)static修饰成员函数
与普通的成员函数相比,静态成员函数由于不是与任何的对象相联系,因此它不具有this指针。从这个意义上来说,它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,只能调用其他的静态成员函数。
Static修饰的成员函数,在代码区分配内存。

2、C++继承和虚函数
C++多态分为静态多态和动态多态。静态多态是通过重载和模板技术实现,在编译的时候确定。动态多态通过虚函数和继承关系来实现,执行动态绑定,在运行的时候确定。
动态多态实现有几个条件:
(1) 虚函数;
(2) 一个基类的指针或引用指向派生类的对象;
基类指针在调用成员函数(虚函数)时,就会去查找该对象的虚函数表。虚函数表的地址在每个对象的首地址。查找该虚函数表中该函数的指针进行调用。
每个对象中保存的只是一个虚函数表的指针,C++内部为每一个类维持一个虚函数表,该类的对象的都指向这同一个虚函数表。
虚函数表中为什么就能准确查找相应的函数指针呢?因为在类设计的时候,虚函数表直接从基类也继承过来,如果覆盖了其中的某个虚函数,那么虚函数表的指针就会被替换,因此可以根据指针准确找到该调用哪个函数。

3、virtual修饰符
如果一个类是局部变量则该类数据存储在栈区,如果一个类是通过new/malloc动态申请的,则该类数据存储在堆区。 如果该类是virutal继承而来的子类,则该类的虚函数表指针和该类其他成员一起存储。虚函数表指针指向只读数据段中的类虚函数表,虚函数表中存放着一个个函数指针,函数指针指向代码段中的具体函数。
如果类中成员是virtual属性,会隐藏父类对应的属性。

静态变量什么时候初始化

静态变量存储在虚拟地址空间的数据段和bss段,C语言中其在代码执行之前初始化,属于编译期初始化。而C++中由于引入对象,对象生成必须调用构造函数,因此C++规定全局或局部静态对象当且仅当对象首次用到时进行构造

STL的allocator

STL的分配器用于封装STL容器在内存管理上的底层细节。
在C++中,其内存配置和释放如下:
new运算分两个阶段:(1)调用::operator new配置内存;(2)调用对象构造函数构造对象内容
delete运算分两个阶段:(1)调用对象析构函数;(2)掉用::operator delete释放内存

为了精密分工,STL allocator将两个阶段操作区分开来:
内存配置由alloc::allocate()负责,内存释放由alloc::deallocate()负责;
对象构造由::construct()负责,对象析构由::destroy()负责。

同时为了提升内存管理的效率,减少申请小内存造成的内存碎片问题,SGI STL采用了两级配置器,当分配的空间大小超过128B时,会使用第一级空间配置器,使用malloc()、realloc()、free()函数进行内存空间的分配和释放;当分配的空间大小小于128B时,将使用第二级空间配置器,采用了内存池技术,通过空闲链表来管理内存。

malloc的原理,另外brk系统调用和mmap系统调用的作用分别是什么?

Malloc函数用于动态分配内存。为了减少内存碎片和系统调用的开销,malloc其采用内存池的方式,先申请大块内存作为堆区,然后将堆区分为多个内存块,以块作为内存管理的基本单位。当用户申请内存时,直接从堆区分配一块合适的空闲块。Malloc在申请内存时,一般会通过brk或者mmap系统调用进行申请。其中当申请内存小于128K时,会使用系统函数brk在堆区中分配;而当申请内存大于128K时,会使用系统函数mmap(内存映射)在映射区分配。

C++ STL 的内存优化

1)二级配置器结构
STL内存管理使用二级内存配置器。
1、第一级配置器 分配的区块大于128bytes
第一级配置器以malloc(),free(),realloc()等C函数执行实际的内存配置、释放、重新配置等操作,并且能在内存需求不被满足的时候,调用一个指定的函数。
2、第二级配置器 分配的区块小于128bytes
在STL的第二级配置器中多了一些机制,避免太多小区块造成的内存碎片,小额区块带来的不仅是内存碎片,配置时还有额外的负担。
内存池管理: 每次配置一大块内存,并维护对应的16个空闲链表(free-list)。下次若有相同大小的内存需求,则直接从free-list中取。如果有小额区块被释放,则由配置器回收到free-list中。
当用户申请的空间小于128字节时,将字节数扩展到8的倍数,然后在自由链表中查找对应大小的子链表
如果在自由链表查找不到或者块数不够,则向内存池进行申请,一般一次申请20块
如果内存池空间足够,则取出内存
如果不够分配20块,则分配最多的块数给自由链表
如果一块都无法提供,则把剩余的内存挂到自由链表,然后向系统heap申请空间,如果申请失败,则看看自由链表还有没有可用的块,如果也没有,则最后调用一级空间配置器

2)二级内存池
在这里插入图片描述
二级内存池采用了16个空闲链表,这里的16个空闲链表分别管理大小为8、16、24……120、128的数据块。这里空闲链表节点的设计十分巧妙,这里用了一个联合体既可以表示下一个空闲数据块(存在于空闲链表中)的地址,也可以表示已经被用户使用的数据块(不存在空闲链表中)的地址。
如何分配内存:
它的内存分配主要分为以下几种情况:
1、对应的free_list不为空
所需空间大小提升为8的倍数后(如需要13bytes空间,我们会给它分配16bytes大小),所对应的free_list不为空时,直接从对应的free_list中拔出,第一位置向后移动指向。
2、对应的free_list为空,其内存池不为空时:
(1)先检验它剩余空间是否够20个节点大小(即所需内存大小(提升后) *20),若足够则直接从内存池中拿出20个节点大小空间,将其中一个分配给用户使用,另外19个当作自由链表中的区块挂在相应的free_list下,这样下次再有相同大小的内存需求时,可直接从 free-list 中拨出。
(2)如果不够20个节点大小,则看它是否能满足1个节点大小,如果够的话则直接拿出一个分配给用户,然后从剩余的空间中分配尽可能多的节点挂在相应的free_list中。
(3)如果连一个节点内存都不能满足的话,则将内存池中剩余的空间挂在相应的free_list中(找到相应的free_list),然后再给内存池申请内存。
3、内存池为空,申请内存
此时二级空间配置器会使用malloc()从heap上申请内存。
4、malloc没有成功
在第三种情况下,如果malloc()失败了,说明heap上没有足够空间分配给我们了,这时,二级空间配置器会从比所需节点空间大的free_list中一一搜索,从任意一个比它所需节点空间大的free_list中拔除一个节点来使用。
5、查找失败,调用一级空间配置器
释放内存
用户调用deallocate释放内存空间,如果要求释放的内存空间大于128bytes,直接调用free。
否则按照其大小找到合适的自由链表,并将其插入。

new/delete和 malloc/free的区别

1、new/delete是C++关键字支持重载,需要编译器支持。malloc/free是库函数,需要头文件stdlib.h支持。
2、new分配内存按照数据类型进行分配,malloc分配内存按照指定的大小分配;
3、new返回的是指定对象的指针,而malloc返回的是void*,因此malloc的返回值一般都需要进行类型转化。
4、new不仅分配一段内存,而且会调用构造函数,malloc不会。
5、new分配的内存要用delete销毁,malloc要用free来销毁;delete销毁的时候会调用对象的析构函数,而free则不会。
6、malloc分配的内存不够的时候,可以用realloc扩容。扩容的原理?new没用这样操作。
7、new如果分配失败了会抛出bad_malloc的异常,而malloc失败了会返回NULL。
8、new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。
自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆

1
2
3
4
5
6
7
8
9
10
11
//申请ArraySize大小的数组
int* Array = (int*)malloc(ArraySize * sizeof(int)); // 在堆中申请内存
memset(Array, 0, ArraySize * sizeof(int)); // 初始化数组全为 0
free(Array); // 释放内存

int* Array = new int[ArraySize]; // 申请内存,不初始化
//int* Array = new int[ArraySize](); // 初始化数组全为 0
//int* Array = new int[5]{ 1,2,3,4,5 }; // 初始化数组为 1,2,3,4,5(VS2015支持)
//int* Array = new int[5]{ 1 }; // 初始化数组为 1,0,0,0,0
memset(Array, 1, ArraySize * sizeof(int)); // 初始化数组全为 1
delete[] Array; // 释放内存

memory leak,内存泄漏

内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。可以使用Valgrind, mtrace进行内存泄漏检查。

内存泄漏的分类:
1.堆内存泄漏 (Heap leak)。对内存指的是程序运行中根据需要分配通过malloc,realloc new等从堆中分配的一块内存,再是完成后必须通过调用对应的 free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak.
2.系统资源泄露(Resource Leak)。主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。
3.没有将基类的析构函数定义为虚函数。当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。

STL有什么基本组成

算法、容器、迭代器

STL中map与unordered_map

底层实现:
map底层是基于红黑树实现的,因此map内部元素排列是有序的。而unordered_map底层则是基于哈希表实现的,因此其元素的排列顺序是杂乱无序的。
map :
优点:
1)有序性,这是map结构最大的优点,其元素的有序性在很多应用中都会简化很多的操作
2)map的查找、删除、增加等一系列操作时间复杂度稳定,都为O(logn )
缺点:查找、删除、增加等操作平均时间复杂度较慢,与n相关
unordered_map:
优点:
查找、删除、添加的速度快,时间复杂度为常数级O(c )
unordered_map 缺点:
因为unordered_map内部基于哈希表,以(key,value)对的形式存储,因此空间占用率高
unordered_map的查找、删除、添加的时间复杂度不稳定,平均为O(c ),取决于哈希函数。极端情况下可能为O(n)

STL中迭代器的作用,有指针为何还要迭代器

Iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。
迭代器和指针的区别:
迭代器不是指针,是类模板,表现的像指针。他模拟了指针的一些功能,通过重载了指针的一些操作符,->、*、++、- -等,提供了比指针更高级的行为,可以根据不同类型的数据结构来实现不同的++,- -等操作。

STL里resize和reserve的区别

resize():改变当前容器内含有元素的数量(size())

1
2
3
vector<int>v{1 ,2}; 
v.resize(4);
v.push_back(5); //1 2 0 0 5

v的size变为len,如果原来v的size小于len,那么容器新增(len-size)个元素,元素的值为默认为0.当v.push_back(3);之后,则是3是放在了v的末尾,即下标为len,此时容器是size为len+1;

reserve():改变当前容器的最大容量(capacity)
如果reserve(len)的值大于当前的capacity(),那么会重新分配一块能存len个对象的空间,然后把之前v.size()个对象通过copy construtor复制过来,销毁之前的内存;

注意:当v.resize(len)中len>v.capacity(),则v中的size和capacity均设置为len;

当v.resize(len) 中len<=v.capacity(),则v中的size设置为len,而capacity不变;
如果此时len<v.size(),那么多出的那些对象(v[len], v[len+1]…)会被销毁,v[0]-v[len-1]仍保留在原地;
当v.reserve(len)中len<=v.capacity(),则v中的capacity不变,size不变,即不对容器做任何改变。

1
2
3
4
5
6
7
8
9
10
vector<int> a;
a.reserve(100);
a.resize(50);
cout<<a.size()<<" "<<a.capacity()<<endl; //50 100
a.resize(150);
cout<<a.size()<<" "<<a.capacity()<<endl; //150 150
a.reserve(50);
cout<<a.size()<<" "<<a.capacity()<<endl; //150 150
a.resize(50);
cout<<a.size()<<" "<<a.capacity()<<endl; //50 150

size可能改变capacity,但capacity不会改变size

C++函数重载、覆盖、隐藏

重载:
函数重载:两个函数名相同,但是参数列表不同(个数,类型),返回值类型没有要求,在同一作用域中
运算符重载:Box operator+(const Box&); //返回类型 Box
覆盖/重写:
子类继承了父类,父类中的函数是虚函数,在子类中重新定义了这个虚函数,这种情况是重写
隐藏:
派生类的函数屏蔽了与其同名的基类函数。

不能重载的运算符:
“.” “.*” “?:” “sizeof()” “typeid()” “static_cast<>” “dynamic_cast<>” “const_cast<>” “#” “##”

C++编译时多态与运行时多态

多态的实现主要分为静态多态和动态多态,静态多态主要是重载,在编译的时候就已经确定;动态多态是用虚函数机制实现的,在运行期间动态绑定。
一个父类类型的指针指向一个子类对象时候,使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数。
静态多态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int Add(int left, int right)
{
return left + right;
}
double Add(double left, int right)
{
return left + right;
}

int main()
{
Add(10, 20);
Add(10.0,20); //正常代码
return 0;
}

动态多态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Father{
public:
virtual void Say(){ //重点!!!
cout << "Father say hello" << endl;
}
};

class Son:public Father{
public:
void Say(){
cout << "Son say hello" << endl;
}
};

void main(){
Father *pFather;
Son son;
pFather = &son;
pFather->Say(); //Son say hello
}

虚函数的实现

在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。

如何实现只能动态分配类对象,不能静态分配

动态分配就是用运算符new来创建一个类的对象,在堆上分配内存。
静态分配就是A a;这样来由编译器来创建一个对象,在栈 上分配内存。
1、动态分配(在堆上分配内存)
将类的构造函数和析构函数设为protected属性,这样类对象不能够访问,但是派生类能够访问,能够正常的继承。同时创建另外两个create和destory函数类创建对象。(将create设为static原因是:创建对象的时候是A *p = A::create(); 只有静态成员函数才能够通过类名来访问。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A  
{
protected:
A(){}
~A(){}
public:
static A* create()
{
return new A();
}
void destory()
{
delete this;
}
};

2、只能静态创建对象
只有使用new运算符,对象才会被建立在堆上。因此只要限制new运算符就可以实现类对象只能建立在栈上。可以将new运算符设为私有,实现如下:

1
2
3
4
5
6
7
8
9
class A  
{
private:
void* operator new(size_t t){}
void operator delete(void* ptr){}
public:
A(){}
~A(){}
};

C++中类成员的访问权限

C++通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。
在类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制。
在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员。

无论共有继承、私有和保护继承,私有成员不能被“派生类”访问,基类中的共有和保护成员能被“派生类”访问。
对于共有继承,只有基类中的共有成员能被“派生类对象”访问,保护和私有成员不能被“派生类对象”访问。对于私有和保护继承,基类中的所有成员不能被“派生类对象”访问。

C++类内可以定义引用数据成员吗?

可以,必须通过成员函数初始化列表初始化。

动态链接库与静态链接库的区别

1、 静态链接库的后缀名为lib,动态链接库的导入库的后缀名也为lib。不同的是,静态库中包含了函数的实际执行代码,而对于导入库而言,其实际的执行代码位于动态库中,导入库只包含了地址符号表等,确保程序找到对应函数的一些基本地址信息;
2、由于静态库是在编译期间直接将代码合到可执行程序中,而动态库是在执行期时调用DLL中的函数体,所以执行速度比动态库要快一点;
3、 静态库链接生成的可执行文件体积较大,且包含相同的公共代码,造成内存浪费;
4、 使用动态链接库的应用程序不是自完备的,它依赖的DLL模块也要存在,如果使用载入时动态链接,程序启动时发现DLL不存在,系统将终止程序并给出错误信息。而使用运行时动态链接,系统不会终止,但由于DLL中的导出函数不可用,程序会加载失败;
5、 DLL文件与EXE文件独立,只要输出接口不变(即名称、参数、返回值类型和调用约定不变),更换DLL文件不会对EXE文件造成任何影响,因而极大地提高了可维护性和可扩展性,适用于大规模的软件开发,使开发过程独立、耦合度小,便于不同开发者和开发组织之间进行开发和测试。

C++中struct和class的区别

在c中,struct不能包含任何函数, 在C++中struct得到了很大的扩充:1.struct可以包括成员函数2.struct可以实现继承3.struct可以实现多态。在C++中struct和class的区别并不是很大,两者之间有很大的相似性。那么为什么还要保留struct,这是因为C++是向下兼容的,因此C++中保留了很多C的东西。
区别:
1.默认的继承访问权。struct的默认继承权限和默认访问权限是public,而class的默认继承权限和默认访问权限是private。
2.另外,class还可以定义模板类形参,比如template <class T, int i>。

C++接口(抽象类)

如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类,纯虚函数是通过在声明中使用 “= 0” 来指定的。设计抽象类的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class soapBase
{
public:
virtual ~soapBase(){};
virtual void show() = 0;
};

class SFJSoap:public soapBase
{
public:
void show() {cout<<"SFJ Soap!"<<endl;}
};

class XSLSoap:public soapBase
{
public:
void show() {cout<<"XSL Soap!"<<endl;}
};

虚函数与纯虚函数区别

C++的虚函数主要作用是“运行时多态”,父类中提供虚函数的实现,为子类提供默认的函数实现。子类可以重写父类的虚函数实现子类的特殊化。
C++中的纯虚函数更像是“只提供申明,没有实现”,是对子类的约束,是“接口继承”。 C++中包含纯虚函数的类,被称为是“抽象类”。抽象类不能使用new出对象,只有实现了这个纯虚函数的子类才能new出对象。

构造函数、析构函数

构造函数 会在每次创建类的新对象时执行。构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。
使用初始化列表来初始化字段

1
C::C( double a, double b, double c): X(a), Y(b), Z(c){....}

析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。
当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数。如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数(即使自定义了析构函数,编译器也总是会为我们合成一个析构函数,并且如果自定义了析构函数,编译器在执行时会先调用自定义的析构函数再调用合成的析构函数),它也不进行任何操作。所以许多简单的类中没有用显式的析构函数。
如果一个类中有指针,且在使用的过程中动态的申请了内存,那么最好显示构造析构函数在销毁类之前,释放掉申请的内存空间,避免内存泄漏。
类析构顺序:1)派生类本身的析构函数;2)对象成员析构函数;3)基类析构函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Line {
public:
void setLength( double len );
double getLength( void );
Line(); // 这是构造函数声明
~Line(); // 这是析构函数声明

private:
double length;
};

// 成员函数定义,包括构造函数
Line::Line(void){
cout << "Object is being created" << endl;
}
Line::~Line(void) {
cout << "Object is being deleted" << endl;
}

void Line::setLength( double len ) {
length = len;
}

double Line::getLength( void ) {
return length;
}

为什么析构函数必须是虚函数?
将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。

为什么C++默认的析构函数不是虚函数
C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。

静态函数和虚函数的区别

静态函数在编译的时候就已经确定运行时机,虚函数在运行的时候动态绑定。虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销

指针和引用的区别

1
2
int a=1;int *p=&a;
int a=1;int &b=a;

指针是一个变量,存储的是一个地址,指向内存的一个存储单元;而引用跟原来的变量实质上是同一个东西,是原变量的一个别名。
区别:
1、指针有自己的一块空间,而引用只是一个别名;
2、使用sizeof看一个指针的大小是4(32位),而引用则是被引用对象的大小;
3、指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象的引用;
4、作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引 用的修改都会改变引用所指向的对象;
5、可以有const指针,但是没有const引用;
6、指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被改变;
7、指针可以有多级指针(**p),而引用至于一级;
8、指针和引用使用++运算符的意义不一样。

实际上”引用”可以做的任何事情”指针”也都能够做,为什么还要”引用”这东西?
答案是 指针能够毫无约束地操作内存中的任何东西,尽管指针功能强大,但是非常危险。
如果的确只需要借用一下某个对象的”别名”,那么就用”引用”,而不要用”指针”,以免发生意外。

数组和指针的区别

指针 数组
保存数据的地址 保存数据
间接访问数据,首先获得指针的内容,然后将其作为地址,从该地址中提取数据 直接访问数据
通常用于动态的数据结构 通常用于固定数目且数据类型相同的元素
通过Malloc分配内存,free释放内存 隐式的分配和删除
通常指向匿名数据,操作匿名函数 自身即为数据名
# 函数指针
函数指针是指向函数的指针变量。
函数指针本身首先是一个指针变量,该指针变量指向一个具体的函数。这正如用指针变量可指向整型变量、字符型、数组一样,这里是指向函数。C在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数
1
2
3
4
5
6
7
8
9
bool LengthCompare(const string &,const string &)
//想要声明一个指向改函数的指针,只需要用指针替换函数名即可
bool (*pf)(const string&, const string&);//未初始化 
pf=LengthCompare;
//pf=&LengthCompare;
bool b1=pf("hello","goodbye");
bool b2=(*pf)("hello","goodbye");
bool b3=LengthCompare("hello","goodbye");
//三个等价调用

const char *,char *,const char crr[],char drr[]

1
2
3
4
5
6
7
8
9
const char * arr = "123";
//字符串123保存在常量区,const本来是修饰arr指向的值不能通过arr去修改
//但是字符串“123”在常量区,本来就不能改变,所以加不加const效果都一样
char * brr = "123";
//字符串123保存在常量区,这个arr指针指向的是同一个位置,同样不能通过brr去修改"123"的值
const char crr[] = "123";
//这里123本来是在栈上的,但是编译器可能会做某些优化,将其放到常量区
char drr[] = "123";
//字符串123保存在栈区,可以通过drr去修改

C++里是怎么定义常量的?常量存放在内存的哪个位置?

常量在C++里的定义const加上对象类型,常量定义必须初始化。
对于局部常量,存放在栈区;
对于全局常量,编译期一般不分配内存,放在符号表中以提高访问效率;
字面值常量,比如字符串,放在常量区。

sizeof

sizeof和new、delete等一样,是关键字,不是函数或者宏
sizeof返回内存中分配的字节数,它和操作系统的位数有关。例如在常见的32位系统中,int类型占4个字节;但是在16位系统中,int类型占2个字节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
// 在32位系统中不同类型的内存分配
// 基本类型
sizeof(int); // = 4
sizeof(double); // = 8
sizeof(char); // = 1
sizeof(bool); // = 1
sizeof(short); // = 2
sizeof(float); // = 4
sizeof(long); // = 4
// 指针
sizeof(int *); // = 4
sizeof(double *); // = 4
sizeof(char *); // = 4
//函数
void fn() { }
sizeof(fn); // error:函数
sizeof(fn()); // error:不能确定类型
//sizeof对函数调用求值结果是函数返回值类型的大小,函数并不会被调用
int fun() { return 3;}
sizeof(fun); // = sizeof(int) = 4
// 数组
int ai[] = {1, 2}; //数组的sizeof返回整个数组所占的字节数,即(数组元素个数×每个元素所占字节)。
sizeof(ai); // = 2*4 = 8
// 常量字符串与字符数组的内存分配方式相同
char ac[] = "abcd"; //注意数组末尾的字符串终结符'\0'
sizeof(ac); // = 5*1 = 5 //注意!!!
sizeof("abcd"); // = 5*1 = 5
// 数组和指针所占的字节数不同
int *pi = new int[10]; //这是指针
sizeof(pi); // = 4
int ai[10];
int *p = ai; //这还是指针
sizeof(p); // = 4

double* (*a)[3][6]; //看成(double *) (*a)[3][6],即一个3×6的二维数组,数组元素为指针,指向double类型。
sizeof(a); // = 4,a为指向上述二维数组的指针
sizeof(*a); // = sizeof(double *)*3*6 = 72,*a表示上述二维数组
sizeof(**a); // = sizeof(double *)*6 = 24,**a即*(*a),表示double*[6],是元素为double指针的一维数组。
sizeof(***a); // = sizeof(double *) = 4,表示上述一维数组中的第一个元素,元素类型为double指针。
sizeof(****a); // = sizeof(double) = 8,表示上述数组首元素指向的double类型。
// 函数形式参数中的数组会蜕变为指针,原因是数组参数“传址调用”
void aif(int p[]) //参数类型是int[],表示指向int的指针
{
sizeof( p ); // = 4
}
void pif(int (*p)[6]) //参数类型是int (*)[6],表示指向int数组的指针
{
sizeof( p); // = 4
sizeof( *p ); // = sizeof(int)*6 = 24
}
void ppf(int *p[6]) //参数类型是int *[],表示指向int指针的指针
{
sizeof( p ); // = 4
sizeof( *p ); // = 4
}
// 32位 类和结构体的内存分配
// 空类或空结构体占一个字节
class CEmpty { };
sizeof(CEmpty); // = 1
struct SEmpty { };
sizeof(SEmpty); // = 1
// 非空类和结构体所占字节为所有成员占字节的和,但是不包括成员函数和静态成员所占的空间
class CInt : public CEmpty { int i;};
sizeof(CInt); // = 4;

class CFunc { void f(){} };
sizeof(CFunc); // = 1;

struct SInt : SEmpty { static int i;};
sizeof(SInt); // = 1;
// 字节对齐
struct SByte1
{
double d; // 长度8,偏移量为0;存放位置区间[0,7]
char j; // 长度1,偏移量为8;存放位置区间[8]
int a; // 长度4,偏移量12;存放位置区间[12,15]
};
sizeof(SByte1); // = 16

struct SByte2
{
char j; // 长度1,偏移量为0;存放位置区间[0,1]
double d; // 长度8,偏移量8;存放位置区间[8,15]
int a; // 长度4,偏移量16;存放位置区间[16,19]
};
sizeof(SByte2); // = 24,为了凑成8的倍数,填充20~23
//可以通过#pragma pack(n)来设定变量以n字节对齐方式,n=1,2,4,8,16
#pragma pack(push) //保存对齐状态
#pragma pack(4) //设定为4字节对齐
class CByte
{
char c; //长度1 < 4 按1对齐;偏移量为0;存放位置区间[0,1]
double d; //长度8 > 4 按4对齐;偏移量为4;存放位置区间[4,11]
int i; //长度4 = 4 按4对齐;偏移量为12;存放位置区间[12,15]
};
#pragma pack(pop) //恢复对齐状态
sizeof(CByte); // = 16
// 联合
union U
{
int i;
char c;
double d;
};
sizeof(U); // = Max(sizeof(i), sizeof(c), sizeof(d)) = sizeof(d) = 8

空类的大小是多少,如果是派生类呢

为何空类的大小不是0呢?
为了确保两个不同对象的地址不同,必须如此。
类的实例化是在内存中分配一块地址,每个实例在内存中都有独一无二的二地址。同样,空类也会实例化,所以编译器会给空类隐含的添加一个字节,这样空类实例化后就有独一无二的地址了。所以,空类的sizeof为1,而不是0.

1
2
3
4
5
6
7
8
class A{ virtual void f(){} }; //4 具有虚函数表的地址
class B:public A{} //4 具有虚函数的类sizeof都是4

class A{}; //1 A是空类,其大小为1
class B:public virtual A{}; //4

class Father1{}; class Father2{}; //1 1
class Child:Father1, Father2{}; //1 多重继承的空类的大小也是1

何时共享虚函数地址表:
如果派生类继承的第一个是基类,且该基类定义了虚函数地址表,则派生类就共享该表首址占用的存储单元。对于除前述情形以外的其他任何情形,派生类在处理完所有基类或虚基类后,根据派生类是否建立了虚函数地址表,确定是否为该表首址分配存储单元。

1
2
3
4
5
6
7
class X{}; //sizeof(X):1
class Y : public virtual X {}; //sizeof(Y):4
class Z : public virtual X {}; //sizeof(Z):4
class A : public virtual Y {}; //sizeof(A):8
class B : public Y, public Z{}; //sizeof(B):8
class C : public virtual Y, public virtual Z {}; //sizeof(C):12
class D : public virtual C{}; //sizeof(D):16

C语言是怎么进行函数调用的

eip是指令指针,即指向下一条即将执行的指令的地址;
ebp为基址指针,常用来指向栈底;
esp为栈指针,常用来指向栈顶。
假设函数A调用函数B,我们称A函数为”调用者”,B函数为“被调用者”则函数调用过程可以这么描述:
(1)先将调用者(A)的堆栈的基址(ebp)入栈,以保存之前任务的信息。
(2)然后将调用者(A)的栈顶指针(esp)的值赋给ebp,作为新的基址(即被调用者B的栈底)。
(3)然后在这个基址(被调用者B的栈底)上开辟(一般用sub指令)相应的空间用作被调用者B的栈空间。
…执行 B函数的主体机器指令段…
(4)函数B返回后,从当前栈帧的ebp即恢复为调用者A的栈顶(esp),使栈顶恢复函数B被调用前的位置;然后调用者A再从恢复后的栈顶可弹出之前的ebp值(可以这么做是因为这个值在函数调用前一步被压入堆栈)。这样,ebp和esp就都恢复了调用函数B前的位置,也就是栈恢复函数B调用前的状态。

每一个函数调用都会分配函数栈,在栈内进行函数执行过程。调用前,先把返回地址压栈,然后把当前函数的esp指针压栈。
参数压栈顺序:从右到左

C++如何处理返回值
生成一个临时变量,把它的引用作为函数参数传入函数内。
C++中拷贝赋值函数的形参能否进行值传递
不能。如果是这种情况下,调用拷贝构造函数的时候,首先要将实参传递给形参,这个传递的时候又要调用拷贝构造函数。。如此循环,无法完成拷贝,栈也会满。

Hash函数

哈希的过程中需要使用哈希函数进行计算。
哈希函数是一种映射关系,根据数据的关键词 key ,通过一定的函数关系,计算出该元素存储位置的函数。表示为:address = H [key]
几种常见的哈希函数(散列函数)构造方法
直接定址法
取关键字或关键字的某个线性函数值为散列地址。
即 H(key) = key 或 H(key) = akey + b,其中a和b为常数
*
除留余数法** 取关键字被某个不大于散列表长度 m 的数 p 求余,得到的作为散列地址。
即 H(key) = key % p, p < m。
数字分析法
当关键字的位数大于地址的位数,对关键字的各位分布进行分析,选出分布均匀的任意几位作为散列地址
仅适用于所有关键字都已知的情况下,根据实际应用确定要选取的部分,尽量避免发生冲突。
平方取中法
先计算出关键字值的平方,然后取平方值中间几位作为散列地址。
随机分布的关键字,得到的散列地址也是随机分布的。
折叠法(叠加法)
将关键字分为位数相同的几部分,然后取这几部分的叠加和(舍去进位)作为散列地址。
用于关键字位数较多,并且关键字中每一位上数字分布大致均匀。
随机数法
选择一个随机函数,把关键字的随机函数值作为它的哈希值。
通常当关键字的长度不等时用这种方法。
当关键字是整数类型时就可以用除留余数法;如果关键字是小数类型,选择随机数法会比较好。

哈希冲突的解决

加载因子:hash表中已经存储的关键字个数,与可以散列位置的比值,
表示Hsah表中元素的填满的程度.若:加载因子越大,填满的元素越多,好处是,空间利用率高了,但:冲突的机会加大了.反之,加载因子越小,填满的元素越少,好处是:冲突的机会减小了,但:空间浪费多了.冲突的机会越大,则查找的成本越高.反之,查找的成本越小.因而,查找时间就越小.
选用哈希函数计算哈希值时,可能不同的 key 会得到相同的结果,一个地址怎么存放多个数据呢?这就是冲突。
1、开放地址法(前提是散列表的长度大于等于所要存放的元素)
发生哈希冲突后,按照某一次序找到下一个空闲的单元,把冲突的元素放入。
线性探查法
从发生冲突的单元开始探查,依次查看下一个单元是否为空,如果到了最后一个单元还是空,那么再从表首依次判断。如此执行直到碰到了空闲的单元或者已经探查完所有单元。
平方探查法
从发生冲突的单元加上1^2,2^2,3^2,…,n^2,直到遇到空闲的单元
双散列函数探查法
定义两个散列函数,分别为s1和s2,s1的算法和前面一致,s2取一个1~m-1之间并和m互为素数的数。s2作为步长。
更适合于造表前无法确定表长的情况;平均查找长度较短;适合结点规模较大时
2、链地址法
将哈希值相同的元素构成一个链表,head放在散列表中。一般链表长度超过了8就转为红黑树,长度少于6个就变为链表。 缺点:指针需要额外的空间
3、再哈希法
同时构造多个不同的哈希函数,Hi = RHi(key) i= 1,2,3 … k;
当H1 = RH1(key) 发生冲突时,再用H2 = RH2(key) 进行计算,直到冲突不再产生,这种方法不易产生聚集,但是增加了计算时间。缺点:每次冲突都要重新散列,计算时间增加  

设计模式(一):工厂模式

工厂模式的两个最重要的功能:
定义创建对象的接口,封装了对象的创建;使得具体化类的工作延迟到了子类中。
对于工厂模式,为了使其能更好的解决多种情况的问题,将其分为三类:简单工厂模式(Simple Factory),工厂方法模式(Factory Method),抽象工厂模式(Abstract Factory)。
简单工厂模式(Simple Factory)
在这里插入图片描述
简单设计模式存在的目的很简单:定义一个用于创建对象的接口。
缺点:对修改不封闭,新增加产品要修改工厂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <iostream>
using namespace std;
enum PRODUCTTYPE {SFJ,XSL,NAS};
class soapBase
{
public:
virtual ~soapBase(){};
virtual void show() = 0;
};

class SFJSoap:public soapBase
{
public:
void show() {cout<<"SFJ Soap!"<<endl;}
};

class XSLSoap:public soapBase
{
public:
void show() {cout<<"XSL Soap!"<<endl;}
};

class NASSoap:public soapBase
{
public:
void show() {cout<<"NAS Soap!"<<endl;}
};

class Factory
{
public:
soapBase * creatSoap(PRODUCTTYPE type)
{
switch(type)
{
case SFJ:
return new SFJSoap();
break;
case XSL:
return new XSLSoap();
break;
case NAS:
return new NASSoap();
break;
default:break;
}

}
};

int main()
{
Factory factory;
soapBase* pSoap1 = factory.creatSoap(SFJ);
pSoap1->show();
soapBase* pSoap2 = factory.creatSoap(XSL);
pSoap2->show();
soapBase* pSoap3 = factory.creatSoap(NAS);
pSoap3->show();
delete pSoap1;
delete pSoap2;
delete pSoap3;
return 0;
}

工厂模式(Factory Method)
在这里插入图片描述
工厂方法模式的应用并不是只是为了封装对象的创建,而是要把对象的创建放到子类中实现:Factory中只是提供了对象创建的接口,其实现将放在Factory的子类ConcreteFactory中进行
缺点:每增加一种产品,就需要增加一个对象的工厂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include <iostream>
using namespace std;
enum SOAPTYPE {SFJ,XSL,NAS};

class soapBase
{
public:
virtual ~soapBase(){};
virtual void show() = 0;
};

class SFJSoap:public soapBase
{
public:
void show() {cout<<"SFJ Soap!"<<endl;}
};

class XSLSoap:public soapBase
{
public:
void show() {cout<<"XSL Soap!"<<endl;}
};

class NASSoap:public soapBase
{
public:
void show() {cout<<"NAS Soap!"<<endl;}
};

class FactoryBase
{
public:
virtual soapBase * creatSoap() = 0;
};

class SFJFactory:public FactoryBase
{
public:
soapBase * creatSoap()
{
return new SFJSoap();
}
};

class XSLFactory:public FactoryBase
{
public:
soapBase * creatSoap()
{
return new XSLSoap();
}
};

class NASFactory:public FactoryBase
{
public:
soapBase * creatSoap()
{
return new NASSoap();
}
};

int main()
{
SFJFactory factory1;
soapBase* pSoap1 = factory1.creatSoap();
pSoap1->show();
XSLFactory factory2;
soapBase* pSoap2 = factory2.creatSoap();
pSoap2->show();
NASFactory factory3;
soapBase* pSoap3 = factory3.creatSoap();
pSoap3->show();
delete pSoap1;
delete pSoap2;
delete pSoap3;
return 0;
}

抽象工厂模式(Abstract Factory)
在这里插入图片描述
抽象工厂模式:给客户端提供一个接口,可以创建多个产品族中的产品对象 ,而且使用抽象工厂模式还要满足一下条件:
1)系统中有多个产品族,而系统一次只可能消费其中一族产品。
2)同属于同一个产品族的产品以其使用。

Eg:搞两个厂房,一个生产低档的牙膏和肥皂,一个生产高档的牙膏和肥皂。比如,厂房一生产中华牙膏、娜爱斯肥皂,厂房二生产黑人牙膏和舒肤佳牙膏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#include <iostream>
using namespace std;
enum SOAPTYPE {SFJ,XSL,NAS};
enum TOOTHTYPE {HR,ZH};

class SoapBase
{
public:
virtual ~SoapBase(){};
virtual void show() = 0;
};

class SFJSoap:public SoapBase
{
public:
void show() {cout<<"SFJ Soap!"<<endl;}
};

class NASSoap:public SoapBase
{
public:
void show() {cout<<"NAS Soap!"<<endl;}
};

class ToothBase
{
public:
virtual ~ToothBase(){};
virtual void show() = 0;
};

class HRTooth:public ToothBase
{
public:
void show() {cout<<"Hei ren Toothpaste!"<<endl;}
};

class ChinaTooth:public ToothBase
{
public:
void show() {cout<<"China Toothpaste!"<<endl;}
};

class FactoryBase
{
public:
virtual SoapBase * creatSoap() = 0;
virtual ToothBase * creatToothpaste() = 0;
};

class FactoryA :public FactoryBase
{
public:
SoapBase * creatSoap()
{
return new SFJSoap();
}

ToothBase * creatToothpaste()
{
return new HRTooth();
}
};

class FactoryB :public FactoryBase
{
public:
SoapBase * creatSoap()
{
return new NASSoap();
}

ToothBase * creatToothpaste()
{
return new ChinaTooth();
}
};


int main()
{
FactoryA factory1;
FactoryB factory2;
SoapBase *pSoap1 = NULL;
ToothBase *pToothpaste1 = NULL;
pSoap1 = factory1.creatSoap();
pToothpaste1 = factory1.creatToothpaste();
pSoap1->show();
pToothpaste1->show();

SoapBase *pSoap2 = NULL;
ToothBase *pToothpaste2 = NULL;
pSoap2 = factory2.creatSoap();
pToothpaste2 = factory2.creatToothpaste();
pSoap2->show();
pToothpaste2->show();

delete pSoap1;
delete pSoap2;
delete pToothpaste1;
delete pToothpaste2;

return 0;
}

设计模式(二):单例模式

单例模式主要解决一个全局使用的类频繁的创建和销毁的问题。单例模式下可以确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
单例模式有三个要素:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。
单例的实现主要是通过以下两个步骤:
将该类的构造方法定义为私有方法,这样其他处的代码就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例;
在该类内提供一个静态方法,当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用。

懒汉:
懒汉式的特点是延迟加载,比如配置文件,在第一次用到类实例的时候才会去实例化;
懒汉加载,如果并发访问:使用锁机制,防止多次访问,可以这样,第一次判断为空不加锁,若为空,再进行加锁判断是否为空,若为空则生成对象。在访问量较小时,采用懒汉实现。这是以时间换空间。
饿汉:
饿汉式的特点是一开始就加载了,在单例类定义的时候就进行实例化。
由于要进行线程同步,所以在访问量比较大,或者可能访问的线程比较多时,采用饿汉实现,可以实现更好的性能。这是以空间换时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//懒汉式 第一次用到类的实例的时候才回去实例化
class singleton
{
private:
singleton(){} //构造函数私有
static singleton* _instance;
public:
static singleton* Get_instance()
{
if(_instance == NULL){ //判断是否第一次调用
Lock();
if(_instance == NULL){
_instance = new singleton();
}
UnLock();
}
return _instance;
}
};

//饿汉式 单例类定义的时候就进行实例化
class singleton
{
private:
singleton(){} //私有构造函数
public:
static singleton* Get_instance()
{
static singleton _instance;
return &_instance;
}
};

应用场景:
Windows的Task Manager(任务管理器)就是很典型的单例模式,你不能同时打开两个任务管理器。Windows的回收站也是同理。
应用程序的日志应用,一般都可以用单例模式实现,只能有一个实例去操作文件。
读取配置文件,读取的配置项是公有的,一个地方读取了所有地方都能用,没有必要所有的地方都能读取一遍配置。
数据库连接池,多线程的线程池。

0%