c/c++学习总结

<仅供个人复习使用 >

C/C++积累

    • C语言
        • 移植性
        • c编译过程
        • 进制问题
        • 补码问题
        • 浮点数的存储
        • 标准输入问题
        • sscanf
        • volatile关键字
        • 结构体问题
        • 位域问题
        • 指针
        • 二维数组
        • do while(0)的妙用
        • 进程内存四区
        • 宏定义
        • 细节归纳
    • C++
        • 细节
        • vs检测内存泄露
        • c++对c语言的扩展
        • 引用
        • 命名空间和作用域问题
        • c++中const修饰的变量
        • extern "C"
        • 权限问题
        • 对象的初始化
        • 类的组合和继承构造函数调用顺序
        • new、delete、malloc、free
        • static和const修饰类成员
        • 友元函数
        • 运算符重载问题
        • 继承问题
        • 虚基类表和虚函数表
        • 继承中的问题
        • 抽象类问题
        • 指向类成员的指针
        • 其他与类相关问题
        • 模块
          • 函数模板
          • 类模板
        • 模版问题
        • 四种类型的转换
        • 工作中遇到的windows c++编译问题
      • STL
        • 基本理论
        • string
        • vector
        • deque - 双端队列
        • stack
        • queue
        • list
        • set/multiset
        • map/multimap
        • 算法
        • 迭代器问题
        • 适配器问题
      • C++11
        • 新增规则
        • 右值问题
        • 智能指针

C语言

移植性

c语言库函数大多数都是支持POSIX标准,只要符合这个标准,函数在不同的平台下执行的结果才会一致 。

c编译过程
  1. 预处理(宏展开,头文件引入,条件编译,注释去掉等)gcc -E 生成.i文件 。
  2. 编译 gcc -S 生成.s文件 。
  3. 汇编 gcc -c 生成.o文件。
  4. 链接gcc -o 生成可执行文件。
进制问题

如何表示相应的进制数:
八进制 :前面加0
十六进制:前面加0x

如何打印相应的进制数:
打印八进制:o%
打印十六进制:x%

注意,C语言不能直接书写二进制数。

int main()
{
	int a = 123;		//十进制方式赋值
	int b = 0123;		//八进制方式赋值, 以数字0开头
	int c = 0xABC;	//十六进制方式赋值
	//如果在printf中输出一个十进制数那么用%d,八进制用%o,十六进制是%x
	printf("十进制:%d\n",a );
	printf("八进制:%o\n", b);	//%o,为字母o,不是数字
	printf("十六进制:%x\n", c);
	return 0;
}

补码问题

源码和反码不利于数值计算,因此在计算机中,数值一律按照补码来存储 。正数的补码等于自己,负数的补码等于反码+1。
补码的补码等于源码。

负数以16进制形式打印时,以补码的形式展示(因为内存中存的就是补码)。

浮点数的存储

单精度float(能够精确到小数点后6位,4位bit能够精确小数点后1位:
c/c++学习总结_第1张图片

双精度double:
c/c++学习总结_第2张图片
单精度或双精度在存储中,都分为三个部分:

符号位 (Sign):0代表正数,1代表为负数;

指数位 (Exponent):用于存储科学计数法中的指数数据;

尾数部分 (Mantissa):采用移位存储尾数部分;

0.5用二进制表示为:0.1,意为2的-1次幂
0.25用二进制表示为:0.01,以为2的-2次幂
因此,小数点后面的几位即表示2的负几次幂。

8.25转换为二进制即为:1000.01,使用科学计数法:1.00001 * 2 ^ 3。尾数部分为00001,指数位为3。

标准输入问题

gets()不会接受输入的\n ,内存污染(接受超过长度的字符串);
scanf()不会接受\n,内存污染;
fgets()接受\n,并且遇到\n、eof、或者读到n-1个字符时停止;

  • 关于二进制打开文件和文本打开文件的区别
    1.win中\n和\n\r的转换
    2.对于整型数据的存储,数字9二进制占四个字节,文件占一个字节存字符9的ASCII码
    3.等等
sscanf
#include 
int sscanf(const char *str, const char *format, ...);
/*
功能:从str指定的字符串读取数据,并根据参数format字符串来转换并格式化数据。
参数:
	str:指定的字符串首地址
	format:字符串格式,用法和scanf()一样
返回值:
	成功:参数数目,成功转换的值的个数
	失败: - 1
*/

字符串格式(详细用法要百度下,注意的是格式不是正则,不要把两者弄混了):

格式 作用
%*s或%*d 跳过数据
%[width]s 读指定宽度的数据
%[a-z] 匹配a到z中任意字符(尽可能多的匹配)
%[aBc] 匹配a、B、c中一员,贪婪性
%[^a] 匹配非a的任意字符,贪婪性
%[^a-z] 表示读取除a-z以外的所有字符

注:正则表达式是匹配一整段字符串是否符合条件,而这里格式是根据通配符来从一段字符串里面获取需要的字符。不要把两者弄混了。

volatile关键字

防止编译器自己优化
例:
int a = 1; a = 2; a = 3;
如上代码,如果不加volatile,则编译器会直接认为a = 3,不会做其他动作;如果加了volatile,则编译器会逐条执行语句。

结构体问题

定义一个结构体时,不能给结构体中元素赋值。

  • 结构体对其的三个原则
    1. 偏移的对齐单位为结构体中最大成员类型或CPU架构(两者间取小),或者通过#pragma pack(xx) 自己设置。
    2. 成员的偏移是其成员类型大小或者对齐单位大小(两者取小)的整数倍。
    3. 最终大小需要是对齐单位的整数倍。
位域问题

位域变量的声明和结构体变量声明类似,例如

//含有位域变量的结构体bs占两个字节
struct bs
{
	int a:6;
	int :0; //空域,作用是下一个数据从下一个单元(字节)开始存放
	int :2; //无名位域,作用是填充调整位置
	int b:3;
	int c:2;
	/*
	一个位域必须存储在一个类型单位中,不能跨两个类型单位存储(例如char类型的位域只能在一个字节中,
	不能跨两个字节存储),如果一个类型单位所剩的空间不足,应从下一个单元开始存放
	*/
	int d:6;
}bit;
bit.a=1; 
bit.b=7; //位域的赋值最大值不能超过该位域位域长度的最大值
bit.c=1;
//位域的使用和结构成员的使用相同,其一般形式为: 位域变量名.位域名,位域允许用各种格式输出
 printf("%d,%d,%d/n",bit.a,bit.b,bit.c);
指针
  • 指针定义问题
    int ( * fun(int) )(int, int) -->返回值为函数指针(两个int参数,返回值为int的函数)
    等价于
    typedef int (FUN * ) (int, int)
    FUN fun1(int)
    int( * (*p)[10])(int *) p为指向数组的指针,数组中每个元素都是函数指针

  • const修饰指针变量问题
    (1)const char **p
    (2)char* const *p
    (3)char ** const p
    从右往左看,确定const修饰的是那个变量,例如上述:
    (1)const修饰的是char**,即**p(一级指针指向的变量)是常量;
    (2)const修饰的是char*,即*p(二级指针指向的一级指针变量)是常量;
    (3)const修饰的是p,即二级指针变量是个常量。
    注意:指针变量是个常量,和指针变量指向的内容是常量两个概念要区分。

二维数组

c/c++学习总结_第3张图片
一维数组的数组名表示第一个元素的首地址,用指针表示为int *p;
二维数组的数组名表示第一行元素的首地址,用指针来表示为int (*p)[j]。

do while(0)的妙用

1.用于宏定义,避免宏展开导致语法错误
2.可以替代goto语义

进程内存四区

c/c++学习总结_第4张图片

宏定义

c/c++学习总结_第5张图片
特殊宏定义

//	__FILE__			宏所在文件的源文件名 
//	__LINE__			宏所在行的行号
//	__DATE__			代码编译的日期
//	__TIME__			代码编译的时间
#include 
#include 
int main(void)
{
	printf("%s\n", __FILE__);
	printf("%d\n", __LINE__);
	printf("%s\n", __DATE__);
	printf("%s\n", __TIME__);
	
	system("pause");
	return 0;
}

细节归纳
  • c语言中带小数点的数默认是double类型
  • 指向一维数组的指针:int (*p)[n](二维数组数组名为该类型)
  • 双引号的字符串代表首元素的地址
  • 静态局部变量在main函数之前就开辟空间
  • feof问题:站在光标处往后看没有任何东西(包括EOF)即判断到结尾。
  • const修饰的局部变量放在栈区可以修改
  • 异或:异为1,同为0
  • 二维数组做形参会退化成一维数组指针
  • extern c {},用于头文件中,可解决c++变量声明与c语言不一致问题:
    不管是C代码调用C++编译器生成的库函数,还是C++代码调用C编译器生成的库函数,都需要在C++代码一侧对相应的函数进行extern “C”申明
  • 对一维数组a[10]的数组名取地址的类型是(*p)[10];
  • 全局变量在多个文件中使用:一般在别的.c文件中做extern声明
  • size filename 查看可执行文件的内存四区
  • 判断栈的生长方向(类比法:创建两个变量和一个数组类比地址)
  • 判断内端大小端验证:char类型的指针去打印int类型的值
  • 静态库在链接时接入到程序中,动态库在程序运行时接入程序中
  • 用异或交换两个值 a = a ^ b b = a^ b a = a^b 任何数和0异或等于本身,和1异或等于本身的相反数
  • #pragma comment(lib,“./staticlib.lib”)vs中使用静态库
  • 关于对于自定义数据结构的理解(HString):自定义类型的本质是,对基本类型进行封装,并提供一系列方法操控数据,且数据最终都会保存在基本类型中;
  • __declspec(dllexport) 在头文件函数前加,表示该函数为动态库导出函数(同样也存在动态库内部使用函数,非导出);
  • 宏定义:
    作用范围是:从定义到本源文件结束
    可以用#undef 命令终止宏定义的结束
    __FILE__所在的文件名
    __LINE__所在的行
    __DATE__编译的日期
    __TIME__编译的时间
  • 返回值 :将一个指向局部变量的指针作为函数的返回值是有问题的。

C++

细节

类中常数据成员只能在构造函数的初始化列表中初始化(c11新标准例外)
*类的数据成员可以在设计时赋予默认值
*定义一个对象时,首先生成私有成员,在执行构造函数的内容
变量前加mutable修饰,在常函数中可以修改它的值
任何时候在新类中重新定义基类中的一个函数(需同名),在基类中所有的其他版本将被自动隐藏

vs检测内存泄露

#define CRTDBG_MAP_ALLOC
#include
#include
int main()
{
char *p = new char[10];
_CrtDumpMemoryLeaks();
return 0;
}

c++对c语言的扩展

1.作用域控制:作用域符::和namespace
2.全局变量检测增强,int a = 10; int a; c++编译不能通过
3.变量类型检测增强
4.更严格的类型转换
5.struct增强
6.三目运算符增强(返回值可以作为左值)
7.const存在不同
8.引用
9.内联函数inline(函数声明和定义放在一起)
10.函数重载(参数的顺序、数量、类型)和默认参数

引用

注意点:引用在声明是必须进行初始化、引用一旦声明初始化后不可以更改;
对数组引用:int arr[10 ]

1.typedef int ARR [10]
ARR &ref = arr;

2.int(&ref)[10] = arr; 类比指向一维数组数组的指针:int(*p)[10] = &arr;

命名空间和作用域问题

作用域的声明:作用域解析符号::(c++多了类作用域)
如果命名空间重名会做合并操作
命名空间可以取别名 namespace newname = oldname(newstd = std;)
*全局变量默认是static,具有内链接属性,想在外部使用需要加extern
局部和全局同名是,可以用::value访问全局变量
using的用法:using namespace name、using name::value

c++中const修饰的变量

[const 修饰的全局变量放在常量区] c语言全局const会被存储到只读数据段;c++中全局const声明extern或对变量取地址时,编译器会分配地址,存放在只读数据段bbs;
c++ const修饰的局部变量如用常量赋值,放在符号表,当使用指针去操作它时用一个临时变量来赋予指针;
const修饰的局部变量如用其他局部变量赋值放在栈区,可以用指针修改;
const修饰的自定义变量(结构体)存放在栈区;
c++中出现在函数之外const默认是本文件可见;
const放在函数末尾,一般用在成员函数上,修饰this指针,防止修改成员变量(mutable修饰的成员变量除外);
const修饰的对象只能调用常成员函数;常成员变量和非常成员变量都可以访问;
const和#define的区别:作用域和类型检测;

extern “C”

c++中为了支持函数重载,在编译后会修改函数的名称,因此当c++中链接c编写的函数时会出现错误(找不到函数名),如果在c++中使用c函数,需要加上extern:

#if __cplusplus
extern "C"{
#endif
	void func1();
	int func2(int a,int b);
#if __cplusplus
}
#endif

权限问题

class中默认访问权限为private,struct默认访问权限为public。

对象的初始化

1.调用无参构造
Person p;
不能使用Person p()的形式来调用无参构造

2.调用有参构造
Person p(100);
隐式调用:Person p=100;
隐式调用一般生成匿名对象Person(100),再赋予我们创建的对象,即Person p = Person(100)。

注意,使用匿名对象初始化判断调用哪一个构造函数,要看匿名对象的参数类型,例如,调用拷贝构造 Person p = p1,相当于Person p = Person(p1);调用普通构造Person p = 100,相当于Person p = Person(100);
匿名对象Person(xxx),有变量来接时,为匿名对象Person(xxx),即Person b = Person(xxx);若没有变量来接,相当于Person xxx;

注意:若是在非初始化时调用:p = 100,会有匿名对象Person(100)构造函数和析构函数的开销,此时的赋值还会调用=号重载,在初始化时Person p =100,则没有匿名对象的开销,只会调用一次构造函数

3.拷贝构造
Person p;
Person p1§
拷贝构造函数只会在对象的初始化时调用;
拷贝构造函数的三种情况:对象初始化,函数传参,函数返回值

4.构造函数调用规则
默认情况下,编译器至少为我们提供三个默认构造函数:默认构造函数(无参,函数体为空)、默认析构函数(无参,函数体为空)、默认拷贝构造函数,对类中非静态成员属性简单值拷贝。
如果用户定义了拷贝构造函数,编译器不会提供任何默认构造函数;如果用户定义了普通构造(非拷贝),编译器不在提供默认无参构造,但是会提供默认拷贝构造。

4.类成员的初始化顺序
先调用对象成员的构造函数(指定调用对象成员的某个构造函数,可在构造函数初始化列表中完成),再调用本身的构造函数;和定义成员的顺序有关,和参数列表无关。

5.explicit关键字
禁止通过构造函数进行隐式转换。

类的组合和继承构造函数调用顺序

1.调用父类构造函数
2.调用成员变量构造函数
3.调用自身构造函数

new、delete、malloc、free
  • 申请数组
    type *p = new type [num];
    delete [ ] p;
  • 申请并初始化
    type *p = new type(xxx);
  • 混用
    通常情况下(非自定义类型变量),理论上混合使用不会出现问题,但是都不会这么去用,对于类对象还可能出现问题。
  • 区别:
    new/delete是c++运算符,对于申请自定义类型变量空间时,会调用其构造函数,删除空间时会调用其析构函数;
    new失败会跑出异常,maolloc失败返回nullptr。
static和const修饰类成员

静态成员变量:为整个类共有,不属于某个对象,不位于对象的存储空间。
静态成员函数:不可以访问普通成员变量。

const静态成员属性:定义静态const数据成员时,最好在类内部初始化。
const修饰成员函数时,const修饰this指针指向的内存区域,函数体内不可以修改类的任何普通成员变量(mutable修饰的普通成员变量除外)。
const修饰的常对象只能调用const的成员函数。

友元函数
  • 全局友元函数
class A
{
public:
	A(int i=0):m_i(i){}
	int GetI()const
	{
		return m_i;
	}
	friend void Add(A& a, A& b);//将Add函数作为类A的友元,在类外部定义,(不能在类A中定义)
protected:
private:
	int m_i;
};
//想在Add函数中访问私有数据成员,又不想通过接口(公有函数)
void Add(A& a, A& b)
{
	cout << a.m_i + b.m_i << endl;
	//cout << a.GetI() + b.GetI() << endl;//接口
}
void main()
{
	A a(5);
	A b(8);
	Add(a, b);
}
//假如Add函数的参数为两个不同的类,即作为多个类的友元函数,需要在这几个类中都做友元函数声明
  • 成员友元函数
class A;
class B
{
public:
	B(int j = 0) :m_j(j) {}
	void Sub(A& a, B& b);
	void Print(A& a);
private:
	int m_j;
};
class A
{
public:
	A(int i = 0) :m_i(i) {}
	friend void B::Sub(A& a, B& b, C& c); //类A中声明Sub和Print为自己的友元函数
	friend void B::Print(A&a);
private:
	int m_i;
};

void B::Sub(A& a,B&b)
{
	cout << a.m_i - m_j << endl;
}
void B::Print(A&a)
{
	cout << a.m_i << endl;
}
void main()
{
	A a(10);
	B b(20);
	b.Sub(a,b);
	b.Print(a);
}
  • 友元类
class B;
class A
{
public:
	A(int a = 0) :m_a(a) {}
	void print(B& b);
private:
	int m_a;
};
class B
{
public:
	B(int b=0):m_b(b){}
	friend class A;//类A作为类B的友元函数,(在B中写friend class A的意思就是:告诉B,A是它的friend),所以类A可以访问类B中的所有成员。
private:
	int m_b;
};
 
void A::print(B& b)
{
	cout << "a::print:" << b.m_b << endl;
}
void main()
{
	A a(2);
	B b(10);
	a.print(b);
}

注意:友元函数不是类的成员函数,只是一种声明,告诉类这是它的朋友,并且它不受其在类体中的 public private 和 protected 区的影响

运算符重载问题
  • 后置++和前置++
    后置++比前置的++的效率低的原因:后置++有一个创建临时对象来保存值的过程,然后再++,返回保存的值
    对于二元运算符来说,重载函数只需要一个参数,对象本身做左耳(即在运算符的左边)

  • 重载函数参数
    非成员函数,一元运算符一个参数,二元运算符两个参数
    成员函数,一元运算符没有参数,二元运算法符一个参数(类对象用作左耳参数)

  • 不能重载的运算符
    && 、||无法实现短路规则

  • 必须重载为成员函数的运算符
    ()、->、[ ]、=

  • 全局函数重载
    << 和 >>只能通过全局函数配合友元函数进行重载 (因为对象只能做左耳,在左耳操作数类固定但又非本类时,只能使用全局函数重载)。
    二元运算符重载一般都为非成员函数。

继承问题
  • 继承中父类构造函数问题
    先调用父类的构造函数,然后再调用子类构造函数;如果父类没有无参构造函数,需要在子类的构造初始化列表中显示调用父类构造函数;
    父类的构造函数和析构函数是不能被继承的,重载的=也无法被继承到子类中。
  • 子类中构造函数调用顺序
    父类构造函数 - 子类对象成员构造函数 - 子类构造函数
  • 子类和基类函数同名
    定义一个和基类同名函数(或重载函数),基类中其他版本均被隐藏;静态成员函数也是如此
  • 静态成员函数
    静态成员函数可以被继承、静态成员函数不能是虚函数
  • 多继承二义性
    可以使用obj.class::value的方式来访问,避免二义性
  • 继承中的权限控制
    c/c++学习总结_第6张图片
虚基类表和虚函数表
  • 虚基类表
    1.虚继承时,虚基类是被共享的,即所有子类共享一份虚基类。每一次继承子类中都必须书写初始化虚基类的语句,但虚基类的初始化最终由最后的子类完成;

    2.普通菱形继承时,最终子类中保存着两份基类的数据;而虚菱形继承时,最终子类保存着两份指向虚基类表指针(分别从两个父类继承而来)和一份虚基类数据;

    3.虚继承类中包含一个虚基类表指针,指向虚基类表;
    4.虚基类表中第一个值为0,后面的每一个值都表示虚基类表指针相对于基类值的偏移大小
    https://blog.csdn.net/longlovefilm/article/details/80558879。

  • 虚函数表
    1.当发现类中包含虚函数时,会创建一个张虚函数表(类中用虚函数表指针保存);

    2.虚函数表中保存了基类和子类的虚函数;

    3.若子类重写了基类的虚函数,虚函数表中会换成子类的定义的函数(重写基类虚函数必须保证函数返回值、参数一致,虚析构函数除外);

    4.以此来达到晚绑定的效果。

继承中的问题

继承父类后,子类中有和父类同名的函数和变量,子类会屏蔽所有父类中同名的变量(普通变量和函数);

虚基类解决菱形继承的问题:最终子类中有两个虚表,分别继承于两个父类,通过虚表中的指针他们共用祖先基类的变量。
可以通过虚表来获取祖先的变量,续表格式 :前四个字节为零,后四个字节为相对偏移量;

virtual虚函数可以起到晚绑定的作用;

基类析构函数不定义为虚析构函数,当我们用基类指针指向子类对象的时候,只会调用基类析构函数;

纯虚析构函数在c++中是合法的,但是在使用的时候有一个额外的限制:必须为纯虚析构函数提供一个函数体;纯虚析构函数和非纯析构函数之间唯一的不同之处在于纯虚析构函数使得基类是抽象类,不能创建基类的对象。

抽象类问题

类中声明了纯虚函数的类,称为抽象类;
抽象类无法实例化;
子类若继承了抽象基类而不去实现纯虚函数也为抽象类;
抽象类中可以声明普通函数;
纯虚函数可以定义函数体,但是没有任何意义;
接口的定义:类中只有函数的声明(纯虚函数),没有任何数据定义;

指向类成员的指针

成员变量:类型 类名: : * 指针名
成员函数:返回类型 (类名: : * 指针名)(参数列表)

其他与类相关问题

c++常用的释放资源方法:定义一个类,将需要释放的资源放在类的析构函数中;

在类中可以定义一个类自身的引用、指针、和静态成员变量,但是无法将自身作为普通成员变量;

内部类(如果一个类定义在另一个类的内部,这个内部类就叫做内部类)可以访问外部类的私有成员,使用模式相当于友元

模块

C++提供两种模板机制:函数模块和类模板

函数模板
template<class T>
void MySwap(T& a,T& b){
	T temp = a;
	a = b;
	b = temp;
}

int a=0, b=0;
//隐式自动推导类型
MySwap(a,b);
//显式指定类型
MySwap<int>(a,b);

函数模块和普通函数在一起调用规则:
 优先考虑普通函数,其次显式具体化函数,其次是模板函数
 可以通过空模板实参列表(Func<>(arg))的语法限定编译器只能通过模板匹配。
 函数模板可以像普通函数那样可以被重载(即定义重载形式的模块)。
 如果函数模板可以产生一个更好的匹配,那么选择模板(普通函数的参数需要进行隐式转换时)。

函数模板机制结论:
 函数模板通过具体类型产生不同的函数。
 编译器会对函数模板进行两次编译,在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。

类模板
//告诉编译器这个函数模板是存在(方法2必不可少的一部分)
template<class T1, class T2> class Person;
template<class T1, class T2> void PrintPerson2(Person<T1, T2>& p);

//类模板用于实现类所需数据的类型参数化
template<class NameType, class AgeType>
class Person
{
public:
	Person(NameType name, AgeType age)
	{
		this->mName = name;
		this->mAge = age;
	}
	//类内实现函数
	void showPerson()
	{
		cout << "name: " << this->mName << " age: " << this->mAge << endl;
	}
	//方法一. 友元函数在类内实现
	friend void PrintPerson(Person<T1, T2>& p)
	{
		cout << "Name:" << p.mName << " Age:" << p.mAge << endl;
	}
	//方法二.友元函数类外实现
	//告诉编译器这个函数模板是存在
	friend void PrintPerson2<>(Person<T1, T2>& p);

public:
	NameType mName;
	AgeType mAge;
};
//继承类模板的时候,必须要确定基类的类型
class xiaoming : public Persion<string, int>
{}
//类外实现函数
template<class T1, class T2>
void Person<T1, T2>::showPerson(){
	cout << "Name:" << this->mName << " Age:" << this->mAge << endl;
}

// 类模板碰到友元函数(方法2)
//友元函数类外实现  加上<>空参数列表,告诉编译去匹配函数模板
template<class T1 , class T2>
void PrintPerson2(Person<T1, T2>& p)
{
	cout << "Name2:" << p.mName << " Age2:" << p.mAge << endl;
}


//类模板不能进行类型自动推导,Person P1("德玛西亚", 18)是不合法的
Person<string, int>P1("德玛西亚", 18);

类模板的声明和实现放到一个文件中,我们把这个文件命名为.hpp(这个是个约定的规则,并不是标准,必须这么写)。

模版问题
template< class T >
void show(T a){ ... }
  • 模版具体化
    template<> void show(int a){}
    具体化指的是为某些特例提供实例(特例类型处理逻辑可能与模块声明中不一致),优先于常规模版,要给出定义

  • 模版显示实例化(2种方法)
    1.template void show< int >(int a) ;无需要函数定义,只需要声明,编译器会自动去生成定义;
    2.show< int >(2),调用时显示实例化;

  • 模板隐式实例化
    show(2)调用时隐式实例化;
    隐式实例化是编译器在遇到模版函数的使用时,根据实参的类型自动生成函数定义。

模板的实例化,可以生成模板实例(类比对象是类的实例),即生成具体函数的声明。

  • 注意
    1.试图在同一个文件(或转换单元)中使用同一种类型的显示实例化和显示具体化声明,会出错。
    2.类模板派生时,必须确定基类的大小(给出基类的具体类型);
    3.模版有两次定义,即编译器会对模版进行两次编译;
    4.类模板和类的定义要写在一个文件中;
    5.类模板碰到友元函数
    方法一:在类内部实现友元函数定义
    方法二:类外实现友元函数定义,有如下三步
    (1)在外面声明友元函数模版;
    (2)在类中友元函数加<>空参数列表,匹配函数模板;
    (3)写(1)的函数模版定义;
    6.函数模板在编译时不会生成任何目标代码,生成实例才会生成目标代码;
四种类型的转换

xxx_cast < type > (variable);

  • static_cast
     用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。
    子类父类的转换:向上转换,安全的、向下转换,没有动态类型检测,不安全。
     用于基本数据类型之间的转换,如把int转换成char,把char转换成int。安全性由开发者保证。

  • dynamic_cast
     dynamic_cast主要用于类层次间的上行转换和下行转换;
     在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;
     在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全;

  • const_cast
     常量指针被转化成非常量指针,并且仍然指向原来的对象;
     常量引用被转换成非常量引用,并且仍然指向原来的对象;

  • reinterpret_cast
    这是最不安全的一种转换机制,最有可能出问题。
    主要用于将一种数据类型从一种类型转换为另一种类型。它可以将一个指针转换成一个整数,也可以将一个整数转换成一个指针。

工作中遇到的windows c++编译问题
  1. 使用vs编译动态库时,库中类的某构造函数包含wchar类型时,库符号表找不到该构造函数的符号声明,解决的方法是:项目属性->c++语言->将WChar视为内置类型;
  2. __declspec(dllexport)和__declspec(dllimport)的区别:dllexport用作制作库文件时导出,dllimport用作使用库文件时导入(非必须,但是当使用库中定义的静态全局变量相关是必须,且能加快效率)

STL

基本理论

STL六大组件:容器、算法、迭代器、函数对象(仿函数)、适配器、空间配置器。

string

1.string和c字符串的转化
string: :c_str()返回的是const char *类型;
string中的字符串不是以’\0’结尾的;

2.string通过[]或at()取得字符的引用后,当整个string重新分配内存后,之前引用可能会无效;

//常用方法
string& operator+=(const string& str/const char c/const char* str);//重载+=操作符
//查找函数集
int find(const string& str, int pos = 0) const; //查找str第一次出现位置,从pos开始查找
int find(const char* s, int pos = 0) const;  //查找s第一次出现位置,从pos开始查找
int find(const char* s, int pos, int n) const;  //从pos位置查找s的前n个字符第一次位置
int find(const char c, int pos = 0) const;  //查找字符c第一次出现位置
int rfind(const string& str, int pos = npos) const;//查找str最后一次位置,从pos开始查找
int rfind(const char* s, int pos = npos) const;//查找s最后一次出现位置,从pos开始查找
int rfind(const char* s, int pos, int n) const;//从pos查找s的前n个字符最后一次位置
int rfind(const char c, int pos = 0) const; //查找字符c最后一次出现位置
//替换
string& replace(int pos, int n, const string& str); //替换从pos开始n个字符为字符串str
string& replace(int pos, int n, const char* s); //替换从pos开始的n个字符为字符串s
//字串
string substr(int pos = 0, int n = npos) const;//返回由pos开始的n个字符组成的字符串

char& operator[](int n);//通过[]方式取字符
char& at(int n);//通过at方法获取字符

vector

1.vector返回的是随机迭代器;
2.所谓动态增加大小,并不是在原空间之后续接新空间(因为无法保证原空间之后尚有可配置的空间),而是一块更大的内存空间,然后将原数据拷贝新空间,并释放原空间。因此,对vector的任何操作,一旦引起空间的重新配置,指向原vector的所有迭代器就都失效了;
3.将容器释放内存的方法: vector< type >().swap( val_name );
4.删除操作会引起后续迭代器失效

//常用操作
push_back(ele); //尾部插入元素ele
pop_back();//删除最后一个元素

at(int idx); //返回索引idx所指的数据,如果idx越界,抛出out_of_range异常。
operator[];//返回索引idx所指的数据,越界时,运行直接报错

erase(const_iterator pos);//删除迭代器指向的元素

resize(int num, elem);//重新指定容器的长度为num,若容器变长,则以elem值填充新位置。如果容器变短,则末尾超出容器长度的元素被删除。
capacity();//容器的容量
reserve(int len);//容器预留len个元素长度,预留位置不初始化,元素不可访问,并且使用push_back时从第一个开始插入。
//遍历删除vector
for (auto iter = a.begin(); iter != a.end(); ) 
{
        if (*iter > 30) 
        {
            iter = a.erase(iter);//删除后后面迭代器会失效,需要用函数返回的
        }
        else
            ++iter;
    }
deque - 双端队列

1.头尾操作数据效率良好,vector头部插入数据效率非常差;
2.虽然deque也提供随机存储迭代器,但是其复杂度和vector的不是一个量级,影响使用效率;
3.deque的实现原理:
(1)deque由许多小段定量的连续内存组成
(2)中控器数组中存放着这些小段内存的地址
c/c++学习总结_第7张图片

4.删除操作会引起后续迭代器失效

//常用操作
push_back(elem);//在容器尾部添加一个数据
push_front(elem);//在容器头部插入一个数据
pop_back();//删除容器最后一个数据
pop_front();//删除容器第一个数据

at(idx);//返回索引idx所指的数据,如果idx越界,抛出out_of_range。
operator[];//返回索引idx所指的数据,如果idx越界,不抛出异常,直接出错。
front();//返回第一个数据。
back();//返回最后一个数据

erase(pos);//删除pos位置的数据,返回下一个数据的位置
stack

Stack所有元素的进出都必须符合”先进后出”的条件,只有stack顶端的元素,才有机会被外界取用。Stack不提供遍历功能,也不提供迭代器。

//常用操作
push(elem);//向栈顶添加元素
pop();//从栈顶移除第一个元素
top();//返回栈顶元素
queue

Queue是一种先进先出(First In First Out,FIFO)的数据结构,它有两个出口,queue容器允许从一端新增元素,从另一端移除元素。queue不提供遍历功能,也没有迭代器。

//常用操作
push(elem);//往队尾添加元素
pop();//从队头移除第一个元素
back();//返回最后一个元素
front();//返回第一个元素
list

1.list本质是一个循环双向链表
2.删除和插入不会导致迭代器失效

//list常用方法
push_back(elem);//在容器尾部加入一个元素
pop_back();//删除容器中最后一个元素
push_front(elem);//在容器开头插入一个元素
pop_front();//从容器开头移除第一个元素

front();//返回第一个元素。
back();//返回最后一个元素。

insert(pos,elem);//在pos位置插elem元素的拷贝,返回新数据的位置。
insert(pos,n,elem);//在pos位置插入n个elem数据,无返回值。
insert(pos,beg,end);//在pos位置插入[beg,end)区间的数据,无返回值

erase(pos);//删除pos位置的数据,返回下一个数据的位置
set/multiset

1.不可以通过迭代器改变set中元素的值
2.set的底层实现是红黑树(平衡二叉树的一种),红黑树是解决二叉搜索树频繁插入后退化成链表问题的
3.set< type, sortfunc > t,sortfunc是自定义排序规则的函数对象

//set常用方法
insert(elem);//在容器中插入元素。
erase(pos);//删除pos迭代器所指的元素,返回下一个元素的迭代器。
find(key);//查找键key是否存在,若存在,返回该键的元素的迭代器;若不存在,返回set.end();
map/multimap

1.对组pair:一对值组合成一个值,这两个值可以有不同的类型;两个值可以用pair的first和second属性访问;

//常用方法
// 第一种 通过pair的方式插入对象
mapStu.insert(pair<int, string>(3, "小张"));
// 第二种 通过pair的方式插入对象
mapStu.inset(make_pair(-1, "校长"));
// 第三种 通过value_type的方式插入对象
mapStu.insert(map<int, string>::value_type(1, "小李"));
// 第四种 通过数组的方式插入值
mapStu[3] = "小刘";

find(key);//查找键key是否存在,若存在,返回该键的元素的迭代器;/若不存在,返回map.end();
erase(pos);//删除pos迭代器所指的元素,返回下一个元素的迭代器。
//遍历删除map元素
map<string,int> testMap;
for(auto it = testMap.begin(); it != testMap.end();)
{
   if(it->second == xxx)
   {
        testMap.erase(it++); //删除后后面迭代器不会失效,但是直接++一个已经删除的迭代器后果是未知的
   }
   else
   {
       it++;
   }
} 
算法
  • 函数对象
    重载了符号()的类,优势:有自己的状态、能够内联编译,性能好;

    假定某个类有一个重载的operator(),而且重载的operator()要求获取一个参数,我们就将这个类称为“一元仿函数”(unary functor);相反,如果重载的operator()要求获取两个参数,就将这个类称为“二元仿函数”(binary functor)。

    函数对象的作用主要是什么?STL提供的算法往往都有两个版本,其中一个版本表现出最常用的某种运算,另一版本则允许用户通过template参数的形式来指定所要采取的策略。

  • 谓词
    普通函数或是重载了()返回值为bool的函数对象;

    如果operator接受一个参数,那么叫做一元谓词,如果接受两个参数,那么叫做二元谓词,谓词可作为一个判断式。

  • 适配器
    (1)函数适配器bind1st/bind2nd,若需要绑定适配器,需要我们的函数对象类根据参数的个数继承unary_function/binary_function;
    (2)取反适配器not1/not2
    (3)函数指针适配器ptr_fun,将普通函数指针适配成函数对象;
    (4)成员函数适配器mem_fun_ref/mem_fun:mem_fun处理对象指针,mem_fun_ref处理对象实体;

迭代器问题

(1)对于节点式容器(map, list, set)元素的删除、插入操作会导致指向该元素的迭代器失效,其他元素迭代器不受影响;
循环删除时采用map.erase(it++);
(2)对于顺序式容器(vector,string,deque)元素的删除、插入操作会导致指向该元素以及后面的元素的迭代器失效;
删除操作会返回下一个元素迭代器;

适配器问题

bind1st( 将参数绑定为函数对象的第一个参数)和bind2nd(将参数绑定为函数对象的第二个参数)要求函数对象必须继承于unary_function/binary_function类型。如果是普通函数想要使用这两个函数,则需要函数指针适配器;

bind1st和bind2nd用作二元函数对象,bind1st表示将迭代器传入到参数1,bind2nd表示将迭代器传入到参数2;

在算法中可以使用函数对象和普通函数,要是想在算法中使用类的成员函数,可以用成员函数适配器来适配成员函数;

四种适配器:绑定(有特殊要求)、指针函数适配器(将普通函数适配成函数对象以适用于绑定)、取反、成员函数适配器(将成员函数适配成函数对象以适用于算法);

//几种适配器的运用方法
for_each(v.begin(), v.end(), bind1st(MyPrint(), x));//MyPrint是一个接受2个参数的函数

find_if(v.begin(), v.end(), not1 ( bind2nd(greater<int>(),5)));//greater是stl内置函数

for_each(v.begin(), v.end(), bind2nd( ptr_fun( MyPrint03 ), 100));//MyPrint03是一个接受2个参数的函数

for_each(v.begin(), v.end(), mem_fun_ref(&Person::ShowPerson));//ShowPerson是一个类成员函数

C++11

新增规则
  • 自动推导变量auto
    本质上auto在c++11中只是一个占位符,声明后必须马上初始化
    错误的使用:auto a = 10, b = 11.5;
    当=号右边是一个引用时,auto推导会把引用忽略,直接推导出它的原始类型;
    当=号右边是const时,auto推导会把const忽略;
    当=号右边是const引用时,auto推导会把引用忽略,保留const;
    auto不能定义数组

  • 自动推导变量decltype
    decltype(exp) varname,与auto不同的是decltype是根据exp来推导类型的,因此它不需要马上初始化,但是必须保证exp表达式可以推导出具体类型;

  • 返回值类型后置
    template
    auto add(T t, U u) -> decltype(t + u)
    {
         return t + u;
    }

  • 使用using来为模板定义别名(typedef想要为模板定义别名需要外敷,形式复杂)

  • 初始化列表
    统一使用大括号{ }来初始化,初始化方式:type a { x } 、 type a = { x },两者等价 ;

  • lambda表达式
    [ 外部变量访问方式说明符 ] ( 参数 ) mutable noexcept/throw() -> 返回值类型
    {
         函数体
    };
    外部变量是指同lambda表达式处于同一作用域的局部变量;
    mutable表示即使参数是const,外部变量是以值传递的方式,也可以修改其值(当值传递时,若不使用mutable,只可以访问,修改会报错;同时以值传递外部变量的修改不会影响外部的值;);
    noexcept/throw() 表示lambda表示是否抛出异常;
    参数和返回值部分都可以省略;但是上述三个任一存在,参数括号不可以省略;

  • for循环改进
    for ( declaration : expression) { }
    declaration :定义一个接受容器内元素的变量;
    expression :需要遍历的容器;
    注意:使用for循环遍历容器时,得到的是容器里的元素,而非迭代器;在for循环遍历容器时,应避免修改容器储存元素个数;

  • constexpr关键字
    constexpr的作用是使得常量表达式具有在编译阶段计算结果的能里,而不必等到运行阶段;例如:
    constexpr int num = 5;
    arr[num] = {0};
    constexpr可以修饰变量、函数、类构造函数、模板函数;但是都分别有需要限制的地方;
    const和constexpr的区别:语义不同,c++11中const强调的是只读属性、constexpre强调的是常量

右值问题
  • 定义
    1.右值引用主要来实现支持" 移动语句 “和” 完美转发 "的;
    2.转移语义可以将资源 ( 堆,系统对象等 ) 从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 应用程序的性能;临时对象的维护 ( 创建和销毁 ) 对性能有严重影响。
    3.用左值引用去接受一个函数返回值时会发生赋值,而右值将会延长临时返回值的生命周期;
    4.右值是指既无名称,也无法取得地址的值

  • 移动语义
    所谓的移动语义,就是指以移动而非深拷贝的方式初始化含有指针成员的对象;简单来说就是将其他对象(通常是临时对象)拥有的内存“移为己用”;

  • std::move()
    将左值强制转换为右值;
    rvalue = move(lvalue)函数可以将左值变为右值;

  • 完美转发
    解决模板函数中接受左值或右值参数问题;
    完美转发规则:
    1、T& + & = T&
    2、T& + && = T&
    3、T&& + & = T&
    4、T或T&& + && = T&&
    template
    void G(T &&a)
    {
    F(forword< T >(a));
    }
    forword函数模板用于修饰被调用函数中需要维持参数左、右值属性的参数;

智能指针
  • share_ptr
    1.多个share_ptr可以共同使用同一块堆内存,并且采用引用计数机制去记录;
    share_ptr< type > val(nullptr);
    share_ptr< type > val (new type(x)) <==>
    share_ptr< type >val = make_shared< type > (x)
    share_ptr< type > val(val1); => val1和val拥有同一块内存,应用计数为2
    2.注意:
    同一普通指针不能为多个share_ptr对象赋值;
    当需要释放数组指针时,需要自定义释放函数;

  • unique_ptr
    每个unique_ptr指向的内存都是独占的,不可以与其他unique_ptr共享;
    基于该类型指针的特性,没有提供拷贝构造函数,只有移动构造函数;

  • weak_ptr
    1.不会单独使用,只能和share_ptr指针搭配使用;
    2.可以使用share_ptr去初始化weak_ptr,但是weak_ptr的释放不会影响到share_ptr的引用计数;
    3.weak_ptr没有重载*和->,因此无法修改share_ptr指针指向的内存,但是可以使用lock()函数来使用share_ptr指向的内存,在使用前最好用expired()函数判断下内存是否有效;
    4.weak_ptr指针是用来解决share_ptr指针循环引用问题的;

你可能感兴趣的:(#,基础总结,c语言,c++)