【c++】c++知识点总结

经过一段时间的学习,c++的学习也结束了,在这里对所学c++知识,做以总结。

一、线性数据结构

首先介绍一下数据结构是什么:

数据结构是计算机存储和组织数据的方式:


我们在这里只讨论线性表中的顺序表和链表


顺序表是指用一段连续存储单元依次存储数据的一种数据存储结构

首先实现静态顺序表,静态顺序表实现的机制就相当于一个大小固定的数组:其定义如下:

#include<iostream>
using namespace std;
#define MAX_SIZE 10//定义数组的大小
typedef int DataType;
// 静态顺序表结构
typedef struct strSeqList
{
	DataType _data[MAX_SIZE];//存放数据的数组
	size_t _iSize;//数组存的元素个数
}SeqList, *PSeqList;

//动态数据结构

typedef struct strSeqList
{
	DataType* _pData;//存放数据的数组
	size_t capacity;//容量
	size_t size;//数组当前元素个数
}SeqList, *PSeqList;



动态顺序表和静态相比,增加了一个成员(容量),存放的数据是当前状态下数组的大小,在每次放入元素时进行判断,如果当前状态下,数组中的元素个数比容量大时,利用动态内存管理函数进行扩容,不再是静态顺序表中直接无法储存。动态顺序表也可以有效利用空间,用多少开辟多少,不像静态顺序表那样,定义空间过大或过小。

在标准库中也有着和顺序表类似的vector标准库类型

链表:


链表拥有很多结点,每个结点前半部分是数据域,后半部分是指针域,指针域指针指向下一个结点;链表可分为单链表、循环链表和双链表。


链表的基本结构:


在标准库同样也存在着一个类似于链表的结构:list

顺序表和链表的对比?

1、顺序表的存储方式是开辟一段连续的空间,大小是固定的(静态顺序表),对数据进行存储和操作,而链表时在空间申请许多个节点,是动态的(动态顺序表的提出就是为了解决这个问题)

2、链表存储比较节省空间一点,顺序表都是一开始就申请很大一片空间来存储数据而链表是存储一个数据申请一个节点。

3、顺序表存储在cpu执行效率上比较强。

顺序表和链表各有优缺点,需要根据情况选择使用:

在查询操作使用的比较频繁时,使用顺序表会好一些;在插入、删除操作使用的比较频繁时,使用单链表会好一些。

二、c和c++的区别:

1、编程模式不同:

c语言是面向过程的,c++基于面向对象的。

面向过程就是分析出解决问题的所需要的步骤,然后用函数把这些步骤一步步实现,使用的时候一个一个一次调用就可以了。

面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描述某个事物在整个解决问题的步骤中的行为。

举个例子来说:

例如设计一个五子棋游戏,面向过程的设计思路就是首先分析问题的步骤:1、开始游戏,2、黑子先走3、绘制画面4、判断输赢5、轮到白子6、绘制画面,7、判断输赢,8、返回步骤2, 9、输出最终结果。把上面每个步骤分别用分别的函数来实现,问题就解决了。

面向对象的设计是从另外的思路来解决问题。整个五子棋双方可以分为1、黑白双方,这两方的行为是一模一样的,2、棋盘系统负责绘制画面,3、规则系统,负责判定诸如犯规、输赢等。第一类对象(玩家对象)负责接受用户输入,并告知第二类对象(期盼对象)棋子布局的变化,棋盘对象接收到了棋子的变化,同时利用第三类对象(规则系统)来对棋局尽心判定。

2、关键字不同

C语言c99标准下有32个关键字,c++下c++98标准下有63个关键

3、文件后缀不同

c语言后缀为:.c

c++后缀为:.cpp

4、语法不同

(1)c语言中若函数没有返回值,则默认返回int,c++中若函数没有返回值,必须将返回值写成void,否则就会报错。

(2)参数列表不同,c语言如果没有参数可传,可不传,c++中若没有参数,传参就会报错,支持半缺省参数,全缺省参数,半缺省参数只能从右向左依次给出

注意:一般情况下,将缺省参数给在声明部分,不能声明定义同时缺省

(3)c++支持函数重载,函数重载:在同一个作用域,有几个功能类似函数名相同,参数列表不同(参数个数、类型次序)和返回值类型无关,是静态多态

函数名+参数类型是c++真正的函数名

5、extern “c”-》将extern“c” 修饰的代码按照c语言风格来编译

6、函数传参不同

c语言穿值传地址,c++传值,传地址,传引用(本质还是传地址,效率高)

三、引用

1、引用的概念

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。

2、引用性质
1. 引用在定义时必须初始化。
2. 一个变量可以有多个引用。
3. 引用一旦绑定了一个实体,就不能再改变为其他变量的引用。

注意:不要返回栈内存的引用

3、引用和指针的区别

【相同点】
底层的实现方式相同,都是按照指针的方式来实现的

【不同点】

1、引用在定义时必须初始化,指针没有要求。

2、一旦一个引用被初始化为指向一个对象,就不能再指向其他对象,而指针可以在任何时候指向任何一个同类型对象
3、没有NULL引用,但有NULL指针。
4、在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数。
5、引用自加改变变量的内容,指针自加改变了指针指向
6、有多级指针,但是没有多级引用
7、引用比指针使用起来相对更安全
Parent &p2 = &c;//指针更为灵活,可以改变指向的对象,方便类型转换,例如p2 = (Child *)p2;这时p2就可以访问子类的所有成员

4、引用使用场景

(1)作为函数参数传入

声明:void function1(int &a)//作用:提高效率,且函数内改变形参a的值对实参有效

使用:int a = 10;

function1(a);//注意传入没有&运算符

常引用声明:void function2(const int &a);//作用:提高效率,且保证函数不会修改实参的值,提高代码的安全性

(2)引用作为函数返回值:

声明:int&  function(...)//作用提高效率,返回的对象不用拷贝(不使用引用,会拷贝副本,将副本返回)

使用:int a;  a=function(...);

使用场景:重载操作符+=, -=, *=, /=等,因为这些运算符返回后赋值给本身。自身“赋值”给自身,使用引用天造地设

class A{...};

A& operator++();//前置++,这也是推荐使用前置++的原因

A&operator--();//前置--

A& operator+=(const A&);

A&operator-=(const A&);

A& operator*=(const A&);

A& operator/=(const A&);

注意事项:禁止将函数中的临时变量作为引用返回(临时变量在函数返回后即被释放,由于二者占用同一块内存,故返回的值随之失效)

例如:int &function(){int a = 10;return a;}//a会在函数返回后被释放

引用与多态,类似于指针

class Parent{...};

class Child:public Parent{...};

Child c;

Parent &p = c;

总之,记住引用和被引用的对象占用同一块内存,不用拷贝,效率高,二者同生共死,使用的时候注意了,记住这一点就知道什么场合该使用引用

四、命名空间

1、概念:

在c++中,变量、函数和类都是大量存在的,这些变量、函数和类的名称都存在于全局命名空间中,会导致很多冲突,使用命名空间的目的是对标识符的名称进行本地化,以

避免命名冲突或名字污染,Namespace关键字的出现就是针对这些问题的。

2、命名空间定义格式

namespace Namespace

{

//内容

}

相同命名空间可以分割在不同的文件中,编译器最后都会合成在一个命名空间下。

namespace Namespace

{

int iTest3;

int iTest4;

}

命名的空间可以嵌套:

namespace Namespace1

{

int iTest0;

int iTest1;

namespace Namespace2

{

int iTest1;

int iTest3;

int iTest4;

}

}

//没有名称的命名空间:

namespace

{}

说明:

本质上讲,一个命名空间就定义了一个范围,在命名空间中定义的任何东西都局限于该命名空间中。没有名称的命名空间可以创建只在声明它的文件中才可见的标识符。也就是
说,只有在声明这个命名空间的文件中,它的成员才是可见的,它的成员才可以被直接访问,不需要命名空间名称来修饰。对于其他文件,该命名空间是不可见的。把全局名称的作用域限制在声明他的文件中的一种方式就是把它的声明为静态的,尽管C++支持全局静态,但最好的方式还是使用未命名的空间。

2、命名空间使用方式:

【命名空间内直接使用】

namespace NamespaceTest

{

int  iTest0;

int  iTest1;

int  iTest2;

void  TestNamespace()

{

iTest0 = 10;

iTest1 = 20;

}


}

【命名空间名称限制】

void TestNamespaceTest

{

Namespace::iTest0 = 10;

Namespace::iTest1 = 20;

void   TestNamespace()

{

iTest0 = 10;

iTest1 = 20;

}

}

【导入命名空间】

using  namespace  Namespace;

void   Namespace

{

iTest0 = 10;

iTest1 = 20;

}

【使用谁引入谁】

using  Namespace::iTest0;

using   Namespace::iTest1;

void   TestNamespace()

{

iTest0 = 10;

iTest1 = 20;

}

六、输出运算符重载

ostream & operator<<(ostream &out, complex &A)
{
    _cout << A.m_real <<" + "<< A.m_imag <<" i ";
    return _cout;
}
一般在类内声明为友元函数,在类外定义。

七、类和对象

1、由结构体引出类

结构体是一系列数据的集合,这些数据可能描述了一个物体,也可能是对一个问题的抽象,举个例子:

typedef struct student
{
char name[20];
int iAge;
char cGender;
}Student, *PStudent;
在c++中结构体不但能存储数据,还能存储函数,在c++中我们常用关键字class代替struct
struct Person
{
char* _pName;
char* _pSex;
unsigned char _cAge;
void Print()
{
cout<<_pName<< "-"<<_pSex<<"-" <<_cAge<<endl;
}
};
int main()
{
Person p0;
p0._pName = "will";
p0._pSex = "男";
p0._cAge = 18;
return 0;
}

class Person
{
char * _pName;
char * _pSex;
unsigned char _cAge;
void Print()
{
cout<<_pName<< "-" <<_pSex<<"-" <<_cAge<<endl;
}
};

【类的真正含义】:类是现实世界的实体在计算机中的反映,它将数据以及在这些数据上的操作封装在一起。

【说明】
1、类定义了一种新类型,是抽象出来描述实体的。
2、类将一组具有相关性数据包装在一体,这些数据称为类的成员变量或属性;类对自己的数据
有特定的操作,这些操作称为类的成员函数或方法。
3、类定义了一种新作用域,它可以选择性的将自己的成员提供给使用者(访问限定符)。
4、类中的任何成员都不能使用auto、extern、register修饰
5、一个类可以有多个成员,类的成员函数可以重载

类两种定义方式:

1、直接在类声明时定义

2、返回值类型 ::函数名(参数列表)

访问限定符有三种:public,private,protected

以class为关键字时,默认访问限定符为:private,以struct 为关键字时默认限定符为:public(兼容c语言)

在c++和c语言中struct的区别,在c++中class和struct的区别:

1、c和c++中struct的主要区别是:

c中的struct不可以含有成员函数,而c++中的struct可以。

2、c++中struct和class的主要区别在于:

(1)默认的存取权限不同,struct默认为public,而class默认为private
(2)class和struct如果定义了构造函数的话,都不能用大括号进行初始化如果没有定义构造函数,struct可以用大括号初始化。如果没有定义构造函数,且所有成员变量全是public的话,可以用大括号初始化。

(3)class继承默认是private继承,而struct继承默认是public继承

3、什么是封装?

隐藏对象的属性和实现细节,仅对外公开接口和对象进行交互,将数据和操作数据的方法进行有机结合。
4、类的作用域

首先来看一下作用域分为哪几种?


类定义了一个新的作用域,类的所有成员都必须处在类的作用域中。形参表和函数体处于类的作用域中。在类体外定义成员,需要使用 :: 作用域解析符指明成员属于哪个类域。在类的作用域外,只能够通过对象或指针借助成员访问操作符.和->来访问类成员,跟在访问操作符后面的名字必须在相关联类的作用域中。

5、类的实例化

(1)概念:用类类型创建对象的过程称为实例化。

1.类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。
2.一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间存储类成员变量。

3、形象的来说类就是建造建筑物所用的图纸,而类的实例化就是用图纸建造建筑物的过程

6、类的对象模型:

概念:对象中各个成员在内存中的布局方式

首先来介绍一下什么是结构体的内存对齐

参见:http://blog.csdn.net/flowing_wind/article/details/75200067

所以每个对象的大小为类中所有成员变量的大小之和,当然这里也遵循内存对齐原则。

7、如何求一个类的大小,空类的大小?

可以用sizeof(类名);直接求类的大小。看下面代码:

#include<iostream>
using namespace std;
class A
{};
class B
{
public:
	B()
	{}
	~B()
	{}

};
class C
{
public:
	C()
	{}
	virtual ~C()
	{}

};

int main()
{
	cout << sizeof(A) << endl;//空类
	cout << sizeof(B) << endl;//带有构造函数和析构函数的类
	cout << sizeof(C) << endl;//带有虚拟函数的类
	system("pause");
	return 0;
}

结果如下图:


空类型的实例中不含有任何信息,本来求sizeof()应该是零,但当我们声明该类型的实例时,它必须在内存中占据一定的空间,否则无法使用这些实例。至于占多少内存取决于编译器,vs中每个空类型的实例占据一字节的空间。

调用析构函数和构造函数时只要知道函数的地址即可,而这些函数的地址也只与类型相关,而与类型的实例无关,编译器也不会因为这两个函数而在实例中添加任何额外的信息。

c++的编译器一旦发现一个类型中有虚拟函数,就会为该类型生成虚函数表,并在该类型的每一个实例中添加一个指向虚函数表的指针,在32位的机器上,一个指针占四个字节的空间,因此求得为4,如果是64位机器,一个指针占8个字节,因此求得结果为8

参考《剑指offer》

8、this指针

【概念】对于一个类的实例来说,你可以看到它的成员函数、成员变量,但是实例本身呢?this是一个指针,它时时刻刻指向这个实例。

【特性】
1、this指针的类型 类类型* const
2、this指针并不是对象本身的一部分,不影响sizeof的结果,没有计算在类大小中。
3、this的作用域在类成员函数的内部(不严谨)。
4、this指针是类成员函数的第一个默认隐含参数,编译器自动维护传递,类编写者不能显式传递。

this指针可以为空吗?

http://blog.csdn.net/starlee/article/details/2062586

9、用c语言来模拟实现c++的类?

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>  
//C 语言没有类,但可以用结构体充当一个类  
//与类不同,结构体只能定义变量,不能够定义函数,可以通过函数指针的方法来实现其功能   
//定义“类 ”的成员变量以及方法   
typedef struct Person{
	char name;
	int age;
	void(*EatFunction)(struct Person this, int num);
}Person;

//定义函数功能   
void EatFunction(struct Person this, int num){
	printf("Test\n");
}

//定义“类 ”的构造函数  
//与面向对象不同,C语言的“类”的 构造函数不能放在“类”中,只能放在“类”外  
//构造函数主要完成 变量的初始化,以及函数指针的赋值   
Person *NewPerson(Person *this){
	this->name = 'A';
	this->age = 18;
	this->EatFunction = EatFunction;
	return this;
}

//主函数调用   
int main(){
	Person person;
	NewPerson(&person);
	person.EatFunction(person, 0);
	system("pause");
	return 0;
}

输出结果如下:


编译器怎样识别一个类呢?

1、识别类名

2、识别类的成员变量

3、识别类中的成员函数和对非静态成员函数进行改写

(1)识别成员函数参数列表:还原this指针

(2)改写函数体:非静态成员前加this

10、类中六个默认成员函数


(1)构造函数

构造函数:是一个特殊的成员函数,名字与类名相同,创建类类型对象时,由编译器自动调用,在对象的生命周期内只且只调用一次,以保证每个数据成员都有一个合适的初始值。

class Date
{
public:
Date()
{}
Date( const int year, const int month, const int
day)
{
_Year = year;
_Month = month;
_Day = day;
}
private:
int _Year;
int _Month;
int _Day;
};
在上面代码中,该类共有两个构造函数,一个带参数,一个不带参数。

【说明】(1)对成员进行初始化时,可使用初始化列表进行初始化。

(2)类中的非静态成员变量只能在初始化列表中出现一次

(3)this用不了

(4)初始化时按照成员在类中声明的次序进行初始化

(5)const、引用、类类型的对象(必须有不是缺省的构造函数)
(6)构造函数的函数体可进行赋值操作

(7)空类不会合成构造函数

(8)什么情况下编译器会合成构造函数?

1、利用类创造对象时,把类中的类对象构造完整

2、在继承体系中,把基类部分构造完成

3、虚拟继承时,虚拟继承和普通继承的区别:填写偏移量表格地址

4、虚函数,填写虚表指针

(9)构造函数有没有返回值?

返回值就是this指针,this指针的类型  类类型  *const  =》是当前对象的引用

构造函数特性:

1、函数名与类名相同。
2、没有返回值。
3、有初始化列表(可以不用)。
4、新对象被创建,由编译器自动调用,且在对象的生命期
内仅调用一次。
5、构造函数可以重载,实参决定了调用那个构造函数。
6、如果没有显式定义时,编译器会提供一个默认的构造函数。
7、无参构造函数和带有缺省值得构造函数都认为是缺省构造函数,并且缺省构造函数只能有一个。

(2)拷贝构造函数

1、概念:只有单个形参,而且该形参是对本类类型对象的引用(常用const修饰),这样的构造函数称为拷贝构造函数。拷贝构造函数是特殊的构造函数,创建对象时使用已存在的同类对象来进行初始化,由编译器自动调用。

2、性质:

(1)它是构造函数的重载。
(2)它的参数必须使用同类型对象的引用传递。
(3)如果没有显式定义,系统会自动合成一个默认的拷贝构造函数。默认的拷贝构造函数会依次拷贝类的数据成员完成初始化。

【说明】拷贝构造函数不给引用不能通过编译,若通过编译则会死循环。

3、使用场景

用已经存在的对象创建新对象时

(3)析构函数

1、概念:析构函数:与构造函数功能相反,在对象被销毁时,由编译器自动调用,完成类的一些资源清理和汕尾工作

2、特性:

a、析构函数在类名(即构造函数名)加上字符~。
b、析构函数无参数无返回值。
c、一个类有且只有一个析构函数。若未显示定义,系统会自动生成缺省的析构函数。
d、对象生命周期结束时,C++编译系统系统自动调用析构函数。
e、注意析构函数体内并不是删除对象,而是做一些清理工作。

3、调用场景

对象被销毁时

11、静态类成员

c语言中


c++中:



八、运算符重载

概念:运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。

基本格式:

返回值  operator +();

赋值运算符:

Test operator=(const  Test& t)
{
    if(this != &t)
{
...
}
return *this;

}
前置++  Test  &operator++() ;    后置++  Test  operator++(int)  

下标运算符的重载

要求必须支持随机访问

T &operator[](size_t   index);

const  T  &operator[](size_t   index)const;

迭代器、T&  operator*();

T&  operator->();

operator==();

operator!=();

输出运算符<<:

(1)若将输出运算符直接写成成员函数,有缺陷,调用的顺序反过来了:

t<<cout;

(2)若将输出运算符定义成普通函数  ostream  operator<<(ostream  &cout,  const   T  &t);  也有缺项,不能在类外访问一个类的私有成员变量。

哪些运算符不能重载?


九、友元

1、概念:

友元函数:友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
友元函数特性:
a、友元函数可访问类的私有成员,但不是类的成员函数;
b、友元函数不能用const修饰;
c、友元函数可以在类定义的任何地方声明,不受类访问限定符限制;
d、一个函数可以是多个类的友元函数;
e、友元函数的调用与普通函数的调用和原理相同;

友元类:友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员;

友元的优缺点
优点:提高了程序运行效率
缺点:破坏了类的封装性和隐藏性
注意:
友元关系不能继承;
友元关系是单向的,不具有交换性;
友元关系不能传递,不是类的成员函数,没有this指针;

为了避免头文件重复包含有以下几种方式:

#pragma once

#ifndef  *****_H

#endif

十、extern关键字总结

1、extern修饰变量和函数:extern修饰的全局变量可以跨文件使用

2、extern"c"被extern"C"修饰的变量和函数是按照C语言方式编译和连接的,未加extern “C”则按照声明时的编译方式

3、extern和static

(1)extern表明该变量在别的地方已经定义过了,在这里要使用那个变量。
(2)static 表示静态的变量,分配内存的时候,存储在静态区,不存储在栈上面。
static作用范围是内部连接的关系这和extern有点相反。它和对象本身是分开存储的,extern也是分开存储的,但是extern可以被其他的对象用extern引用,而static不可以,只允许对象本身用它。具体差别首先,static与extern是一对“水火不容”的家伙,也就是说extern和static不能同时修饰一个变量;其次,static修饰的全局变量声明与定义同时进行,也就是说当你在头文件中使用static声明了全局变量后,它也同时被定义了;最后,static修饰全局变量的作用域只能是本身的编译单元,也就是说它的“全局”只对本编译单元有效,其他编译单元则看不到它,

4、extern和const

当const单独使用时它就与static相同,而当与extern一起合作的时候,它的特性就跟extern的一样了

十一、谈谈你对const的理解

c语言中   const修饰变量,表示变量的内容不能修改。

const   int  count  =   0;

int  array[count]  =   0;

c++:

const  修饰变量  表示常量,具有宏常量的特性,在编译期间进行替换

const  修饰成员变量

const  修饰类成员

const   修饰成员函数,实际情况是const修饰this指针

普通成员函数和const成员函数的区别?

函数后面加const
编译器会自动给每一个函数加一个this指针。在一个类的函数后面加上const后,就表明这个函数是不能改变类的成员变量的(加了mutable修饰的除外,后面有讲)。实际上,也就是对这个this指针加上了const修饰。

mutable可以使被const修饰的变量被修改

volatlile关键字,确保本条指令不会被编译器的优化而省略,且要求每次直接读值

十二、内联函数和宏函数

内联函数和普通函数相比,可以加快程序运行的速度,因为不需要中断调用,在编译的时候内联函数可以直接被镶嵌到目标代码中,而宏只是一个简单的替换。内联函数要做参数类型检查,这是内联函数跟宏定义相比的劣势。inline是指嵌入代码,就是在调用的地方把代码直接写到那里去,而不是跳转。对于短小的代码来说inline增加空间消耗换来的是效率提高,这方面和宏定义一模一样,但是inline在和宏相比没有付出任何额外代价的情况下更加安全,至于是否需要inline函数,就需要根据实际情况来取舍了。
一般内联函数只用于以下情况:
(1)一个函数不断地重复调用;
(2)函数只有简单的几行,且函数内不包含for、while、switch语句

有些情况不适合使用内联函数

1、如果函数体内的代码较长,使用内联函数将导致内存消耗代价较高

2、如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大,inline也不应该出现在函数的声明中;

十二、c++动态内存管理

new->单个元素(new[]->一段连续空间)

delete(delete[])

new  和malloc,delete和free区别?




malloc/free,   new/delete,   new[]/detele[]一定匹配使用,否则会出现内存泄漏,程序崩溃

如何进行内存泄漏检测?简单的内存检测工具

#define _CRTDBG_MAP_ALLOC  
#include <crtdbg.h>  
#include <iostream>  
using namespace std;  
int main(int argc,char** argv)  
{  
    char *str1 = NULL;  
    char *str2 = NULL;  
    str1=new char[100];  
    str2=new char[50];  
  
    delete str1;  
    _CrtDumpMemoryLeaks();  
    return 0;  
}  
内存泄露的关键就是记录分配的内存和释放内存的操作,看看能不能匹配。跟踪每一块内存的声明周期,例如:每当申请一块内存后,把指向它的指针加入到List中,当释放时,再把对应的指针从List中删除,到程序最后检查List就可以知道有没有内存泄露了。Window平台下的Visual Studio调试器和C运行时(CRT)就是用这个原理来检测内存泄露。
在VS中使用时,需加上
#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
crtdbg.h的作用是将malloc和free函数映射到它们的调试版本_malloc_dbg和_free_dbg,这两个函数将跟踪内存分配和释放(在Debug版本中有效)
_CrtDumpMemoryLeaks();
函数将显示当前内存泄露,也就是说程序运行到此行代码时的内存泄露,所有未销毁的对象都会报出内存泄露,因此要让这个函数尽量放到最后。

设计一个类只能在堆上创建对象:

析构函数设为protected可以有效解决这个问题,类外无法访问protected成员,子类则可以访问。不使用new,而是使用一个函数来构造,使用一个函数来析构。

class A  
{  
protected:  
    A(){}  
    ~A(){}  
public:  
    static A*create()  
    {  
        return new A();  
    }  
    void destory()  
    {  
        delete this ;  
    }  
};  

设计一个类只能在栈上创建对象:
只有使用new运算符,对象才会建立在堆上,因此,只要禁用new运算符就可以实现类对象只能建立在栈上。将operator new()设为私有即可。
class A  
{  
private :  
    void *operator new(size_t t){}// 注意函数的第一个参数和返回值都是固定的  
    void operator delete(void* ptr){}// 重载了new就需要重载delete  
public:  
    A(){}  
    ~A(){}  
};  
设计一个类只能创建一个对象:
在类中创建一个静态变量Count,用来限制可创建的实例的数量
#include <iostream>  
  
class SingleClass  
{  
public:  
    static SingleClass* GetSingleClass()    //静态成员函数  
    {  
        if (Count > 0)  
        //如果Count大于0,那么就调用new创建一个类指针,并且计数Count减1,否则返回NULL  
        {  
            Count--;  
            return new SingleClass();  
        }  
        else  
        {  
            return NULL;  
        }  
    }  
private:  
    SingleClass(){};  
    static int Count;   //静态成员变量Count,不允许在类中初始化。定义为const则可以在这初始化,但是不可更改,不适合在这使用  
};  
  
int SingleClass::Count = 1; //Count的初始化,可以自己设置限制创建实例的个数  
  
int main()  
{  
    SingleClass* test;  //只能通过定义类指针来创建类实例  
    test = SingleClass::GetSingleClass();  
    return 0;  
}

操作符new,new操作符重载?

首先要明白我们为什么要重载new操作符,其实就是指针造成的,指针的引入的确给我们带来很多方便,但也带来一些麻烦,小程序可以避免,内存泄漏问题,可是大程序就很难避免了,这时就需要追踪new和delete的动作,找到未释放的内存,这时我们所采用的方法就是重载new操作符。

new是c++内置的和sizeof()一样,我们不能修改不了其中的功能,但我们new一个对象时,事实上就做了两件事;1、开辟出足够大小的空间,使用sizeof()编译器会为我们计算好空间的大小

二、初始化对象,我们一般用括号来初始化对象,比如:int *p = new int (3);然而由于数组不能初始化,对于类对象必须定义无参的构造函数。

new运算符重载一般的格式是:

void  *operator new(size_t size);

在new运算符的重载中,参数可以增加,但第一个参数必须是size_t类型,正是这种参数为我们重载new提供了可能。

void * operator new(size_t size, const char *file, int line)
{
    cout << "new size:" << size << endl;
    cout << file << " " << line << endl;

    void * p = malloc(size);

    return p;
}

然后通过宏替换替换所有new

#define  new  new(_FILE_,_LINE_)

这样我们每次调用new,比如int * pn = new int;被编译器替换成了int * pn = new (__FILE__, __LINE__) int,从而调用我们定义的operator new,这种办法确实很妙。需要交代的是,对于数组同样适用,而是在编译的是后由编译器计算出所需要的长度调用我们定义的operator new函数。

对于delete,我们无需使用宏定义,只要重载operator delete就可以了,然而我们需要重载两个delete:

void operator delete(void * p)
{
    cout << "delete " << (int)p << endl;
    free(p);
}

void operator delete [] (void * p)
{
    cout << "delete [] " << (int)p << endl;
    free(p);
}

其实后面一个函数的内容和前面的内容一样,但为了支持数组的释放,我们必须是要定义的。那么我们只是简单的free(p)编译器咋知道我们释放的数组,还是单个对象了,这个不用我们操心了,我们只要提供给free函数一个申请内存的首地址就可以了。因为在用malloc申请的时候,我们也把数组装换为内存大小了。
既然我们已经跟踪了new和delete,那么就可以比较容易的判断申请的内存是否最后得到释放,要完成它,我们还需要一个链表,或者其它,当我们申请一块内存的时候加入到链表中,释放一块空间的时候,从链表中删除和释放内存首地址相同的节点就可以了,最后理想的情况是链表为空,如果不为空,那就说明内存发生泄露(Memory leaks)了。

完整代码:

#include "malloc.h"
#include "iostream.h"

#ifdef _DEBUG

void * operator new(unsigned int size, const char *file, int line)
{
    cout << "new size:" << size << endl;
    cout << file << " " << line << endl;

    // 下面两种方法可以达到同样的效果,但下面一种比较好
    // 因为用下面一种可以保持原有的申请方式一样
    //void * p = malloc(size);
    void * p = operator new(size);

    return p;
}

void operator delete(void * p)
{
    cout << "delete " << (int)p << endl;
    free(p);
}

void operator delete [] (void * p)
{
    cout << "delete [] " << (int)p << endl;
    free(p);
}

void operator delete(void * p, const char *file, int line)
{
    cout << "delete file line" << endl;
    free(p);
}

void operator delete [] (void * p, const char *file, int line)
{
    cout << "delete [] file line" << endl;
    free(p);
}

#define new new(__FILE__, __LINE__)
#endif

void main()

十三、深浅拷贝

1、概念:

浅拷贝:也称位拷贝,编译器只是直接将指针的值拷贝过来,结果多个对象共用同一块内存,当一个对象将这块内存释放掉之后,另一些对象不知道该块空间已经还给了系统,以为还有效,所以在对这段内存进行操作的时候,发生了访问违规。

深拷贝:拷贝时将整个数据块拷贝过来,并将值拷贝过来,这样拷贝的和被拷贝的各自拥有自己的数据块,释放的时候也释放各自的空间,不会出现错误。

2、举个例子

#include<iostream>   
 #include<cstring>   
 using namespace std;   
 class String {   
 public:   
     String (const char* psz=NULL) : m_psz(strcpy((new char[strlen(psz?psz:"")+1]),psz?psz:"")){   
         cout << "String构造" << endl;   
     }   
      ~String () {   
         if(m_psz) {   
              delete[] m_psz;   
              m_psz = NULL;   
         }   
         cout << "String析构" << endl;   
     }   
     char* c_str(void) {   
         return m_psz;   
     }   
 private:   
     char* m_psz;   
 };   
 int main(void) {   
     String s1("hello");   
     String s2(s1);   
     cout << "s1    " << s1.c_str() << endl;   
     cout << "s2    " << s2.c_str() << endl;   
     s1.c_str()[0] = 'H';   
     cout << "s1    " << s1.c_str() << endl;   
     cout << "s2    " << s2.c_str() << endl;   
     return 0;   
 } 

输出结果:



这就是一个典型的浅拷贝,利用拷贝构造函数构造第二个对象时,两个对象共用同一块空间,在创建对象完成进行析构时,对同一块空间释放了两次,导致程序崩溃。

在利用对象1创建对象2时,由于代码中并没有定义,拷贝构造函数,这时编译器会为我们自己合成一个拷贝构造函数

其形式为:

String (const String& that) {} 

这个缺省构造函数是按字节进行复制的,对于指针变量只复制地址,这就使得两个指针同时指向一片空间,形成浅拷贝。

为了实现深拷贝,就需要我们要自己重新写一个拷贝构造函数。

String (const String& that) : m_psz(strcpy((new char[strlen(that.m_psz)+1]),that.m_psz))
{   
    cout << "String拷贝构造" << endl;   
}  

这时程序就可正常执行,


写时拷贝就是在原空间多加一个空间,添加一个计数器,新开辟的这片空间存储的是当前空间被几个对象使用,每增加一个对象使用这片空间,就对计数器加一,创建对象完成,析构时首先判断计数器是否为为一,若不为一,则只对计数器进行--操作,当计数器为一时再对空间进行释放,这样就提供了另一种能解决浅拷贝的方法

举个例子:例子中——pStr中存的就是计数器

class String  
{  
private:  
    char* _pStr;  
public:  
    String(char* pStr = "")  
    {  
        _pStr = new char[strlen(pStr) + 1 + 4];  
        *(int *)_pStr = 1;  
        strcpy(_pStr + 4, pStr);  
    }  
    String(const String& s)  
    {  
        this->_pStr = s._pStr;  
        (*(int *)(_pStr))++;  
    }  
    String& operator=(String& s)  
    {  
        if (--(*(int *)(_pStr)) == 0)  
        {  
            delete[] _pStr;  
            _pStr = NULL;  
        }  
        this->_pStr = s._pStr;  
        (*(int *)(_pStr))++;  
        return *this;  
    }  
    ~String()  
    {  
        if (0 == --(*(int *)(_pStr)))  
        {  
            delete[] _pStr;  
            _pStr = NULL;  
        }  
    }  
    char& operator[](size_t index)  
    {  
        --(*(int *)(_pStr));  
        char *p = new char[strlen(_pStr + 4) + 1 + 4];//开辟一块一样大的空间~  
        cout << strlen(_pStr) + 1 + 8;  
        (*(int *)(p)) = 1;  
        strcpy(p + 4, _pStr + 4);  
        _pStr = p;  
        return this->_pStr[index + 4];  
    }  
    friend ostream& operator<<(ostream &out, const String &n)  
    {  
        out << n._pStr + 4;//指向的是开辟内存的起始位置,所以要加4  
        return out;  
    }  
    bool operator>(String& s)  
    {  
        if (_pStr == s._pStr)  
            return false;  
        while (*(_pStr+4) != '\0'&&*(s._pStr+4) != '\0')  
        {  
            while((*(_pStr+4)==*(s._pStr+4)))  
            {  
                _pStr++;  
                s._pStr++;  
                if (*(_pStr + 4) == '\0'&&*(s._pStr + 4) == '\0')  
                    break;  
            }  
            if (*(_pStr + 4) > *(s._pStr + 4))  
                return true;  
            else  
                return false;  
        }  
    }  
    bool operator<(String& s)  
    {  
        if (_pStr == s._pStr)  
            return false;  
        while (*(_pStr + 4) != '\0'&&*(s._pStr + 4) != '\0')  
        {  
            while ((*(_pStr + 4) == *(s._pStr + 4)))  
            {  
                _pStr++;  
                s._pStr++;  
                if (*(_pStr + 4) == '\0'&&*(s._pStr + 4) == '\0')  
                    break;  
            }  
            if (*(_pStr + 4) < *(s._pStr + 4))  
                return true;  
            else  
                return false;  
        }  
    }  
    bool operator==(String& s)  
    {  
        if (_pStr == s._pStr)  
            return false;  
        while (*(_pStr + 4) != '\0'&&*(s._pStr + 4) != '\0')  
        {  
            while ((*(_pStr + 4) == *(s._pStr + 4)))  
            {  
                _pStr++;  
                s._pStr++;  
                if (*(_pStr + 4) == '\0'&&*(s._pStr + 4) == '\0')  
                    return true;  
            }  
            return false;  
        }  
    }  
    bool operator!=( String& s)  
    {  
        if (_pStr == s._pStr)  
            return false;  
        while (*(_pStr + 4) != '\0'&&*(s._pStr + 4) != '\0')  
        {  
            while ((*(_pStr + 4) == *(s._pStr + 4)))  
            {  
                _pStr++;  
                s._pStr++;  
                if (*(_pStr + 4) == '\0'&&*(s._pStr + 4) == '\0')  
                    return false;  
            }  
            return true;  
        }  
    }  
    bool strstr(const String& s)  
    {  
        if (_pStr == s._pStr)  
            return true;  
        char *dest=NULL;  
        char *src = NULL;  
        while (*(_pStr + 4) != '\0')  
        {  
            dest = _pStr + 4;  
            src = s._pStr + 4;  
            while (*dest++ == *src++)  
            {  
                if (*src == '\0')  
                {  
                    return true;  
                }  
            }  
            _pStr++;  
        }  
        return false;  
    }  
};  

十四、继承

1、概念

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。

2、继承定义格式:

class  派生类(子类):继承类型   父类

3、继承关系和访问限定符



4、使用继承的好处:

代码复用、实现多态

5、在继承体系中基类和派生类对构造函数和析构函数的调用次序

构造函数:






析构函数:


6、赋值兼容性规则

1. 子类对象可以赋值给父类对象(切割/切片)
2. 父类对象不能赋值给子类对象
3. 父类的指针/引用可以指向子类对象
4. 子类的指针/引用不能指向父类对象(可以通过强制类型转换完成)

7、基类中哪些可以被派生类继承?

构造函数&拷贝构造函数&析构函数&赋值运算符重载

8、同名隐藏

在继承体系中基类和派生类有相同的成员

(1)函数名相同,参数不同,基类没有virtual,基类函数被隐藏,

(2)函数名相同,参数相同,基类没有virtual,基类函数被隐藏

覆盖:派生类函数覆盖基类函数(重写)

(1)不同范围(分别位于基类和派生类)

(2)函数名相同

(3)参数相同

(4)基类函数必须有virtual关键字


9、不同继承体系下的对象模型

对象模型概念:对象中各个成员在内存中的布局格式

单继承


多继承


菱形继承


虚拟继承和普通继承的区别?
假设derived 继承自base类,那么derived与base是一种“is a”的关系,即derived类是base类,而反之错误;
假设derived 虚继承自base类,那么derivd与base是一种“has a”的关系,即derived类有一个指向base类的vptr。
十五、多态

1、概念

多态:一词最初来源于希腊语,意思是具有多种形式或形态的情形,在C++语言中多态有着更广泛的含义
静态多态】:编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推断出要调用那个函数,如果有对应的函数就调用该函数,否则出现编译错误。
【动态多态】
动态绑定:在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方法。使用virtual关键字修饰类的成员函数时,指明该函数为虚函数,派生类需要重新实现,编译器将实现动态绑定。

举个例子:

上厕所就是多态:对于上厕所这一个行为,产生了两种不同的结果,男人去男厕所,女人去女厕所

【动态绑定条件】
(1)必须是虚函数
(2)通过基类类型的引用或者指针调用虚函数,在派生类中,必须对基类虚函数进行重写

2、重写的概念:在继承体系中,基类中包含虚函数,如果在派生类中函数和基类虚函数原型完全相同,(协变&析构函数除外),在派生类的虚表中会用派生类自己的函数替换基类函数相同位置的虚函数。

3、动态多态实现原理

虚表剖析->虚表构造->简单动态多态例子

4、构造函数不能做虚函数,析构函数可以做虚函数

(1)为什么C++不支持普通函数为虚函数?
普通函数(非成员函数)只能被overload,不能被override,声明为虚函数也没有什么意思,因此编译器会在编译时邦定函数。
(2)为什么C++不支持构造函数为虚函数?
这个原因很简单,主要是从语义上考虑,所以不支持。因为构造函数本来就是为了明确初始化对象成员才产生的,然而virtual function主要是为了再不完全了解细节的情况下也能正确处理对象。另外,virtual函数是在不同类型的对象产生不同的动作,现在对象还没有产生,如何使用virtual函数来完成你想完成的动作。(这不就是典型的悖论)
(3)为什么C++不支持内联成员函数为虚函数?
其实很简单,那内联函数就是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的。(再说了,inline函数在编译时被展开,虚函数在运行时才能动态的邦定函数)

(4)、为什么C++不支持静态成员函数为虚函数?
这也很简单,静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码,他也没有要动态邦定的必要性。
(5)为什么C++不支持友元函数为虚函数?
因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。

5、抽象类

含有纯虚函数的类

纯虚函数在成员函数的形参列表后面写上=0,则成员函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。纯虚函数在派生类中重新定义以后,派生类才能实例化出对象。

class Person
{
virtual void Display () = 0; // 纯虚函数
protected :
string _name ; // 姓名
};
class Student : public Person
{};

十六、函数模板

【函数模板】

1、概念:代表了一个函数家族,该函数与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。

2、参数推演-隐式实例化,类型转化

3、模板函数如何编译?

(1)实例化之前,检查模板代码本身,查看是否出现语法错误,如:遗漏分号
(2)在实例化期间,检查模板代码,查看是否所有的调用都有效,如:实例化类型不支持某些函数调用

4、模板参数列表->类型

分为类型形参和非类型形参

注意:template<class  T1,class  T2>

5、支持重载

6、函数模板特化

(1)关键字template后面接一对空的尖括号<>
(2)函数名后接模板名和一对尖括号,尖括号中指定这个特化定义的模板形参
(3)函数形参表
(4)函数体
template<>
返回值 函数名<Type>(参数列表)
{
// 函数体
}
特化的声明必须与特定的模板相匹配

【类模板】

1、定义模板类也是模板,必须以关键字template开头,后接模板形参表。
【模板类格式】
template<class 形参名1, class 形参名2, ...class 形参名n>
class 类名
{ ... };

2、STL--容器(vector顺序容器,list链式容器)

 vector和list区别?使用场景?

区别:(1)存储结构不同,一个是连续空间存储,另一个是链式存储,

(2)vector支持随机访问,list不支持

(3)list实现任意位置插入,删除更加方便,更加高效

使用场景:当要使用随机访问这一功能时采用vector,当要使用任意位置插入删除时,使用list

迭代器 iterator  容器适配器

迭代器:Find(first, last, data)->遍历

int array[10];

Find(array, array+10, 100 );




模板类特化的应用类型萃取:

自定义类型->模板类

内置类型->特化

3、异常

(1)throw 1抛出异常,标记

try{}

catch(类型)

2、栈展开

f1<-f2<-f3<-main

f2调用f1,f3调用f2,f1的异常向下传递到main函数,程序崩溃

3、重新抛出

catch (...)

{

throw;

}

4、不能在构造函数和析构函数抛出异常,除非对异常已经捕获

5、异常规范

  Fun () throw(int);

智能指针->

RAII原理--资源初始化,利用构造函数功能

auto_ptr资源转移

shared _ptr

scoped  _ptr  boost库

unique _ptr  引用计数,资源独占,防拷贝

weak _ptr 为了解决 shared _ptr出现的循环引用问题

循环引用————如何解决?

原理:引用计数

定制删除器——>函数指针、仿函数

两种类型转换:显式类型转换,隐式类型转换

缺点:可视性比较差,所有的转换形式都以一种相同形式书写,难以跟踪错误的转换

关键字:

static_cast:隐式类型转换,不能用于两个不相关的类型进行转换

double b = 1.34;

int *p = (int *)&d;

int  i = static_cast<int >(d);

reincrpret_cast  显式类型转换  一种类型到另一种不同的类型

const_cast  删除变量的const属性

const  int a  = 10;

int  *pa = const_cast<int *>(&a);

pa = 100;

dynamic_cast  将父类对象指针转化为子类对象指针

向上类型:子类对象指针->父类指针/引用(不用转换)

向下转换:父类->子类指针/引用\用dynamic_cast   基类必须有虚函数

RTTI:运行时类型检测

单构造函数可能会类型转换

explicit关键字组织经过转换构造函数进行的隐式转换的发生



  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值