c++面试 查漏补缺

1.在c++程序中调用被c编译器编译后的函数,为什么要加 extern “C”?
在c++程序调用c语言编译的动态库时,c对应的函数头要添加extern “C”。c++语言支持函数重载,c语言不支持函数重载。函数被c++编译后在库中的名字和c语言不同。假设某个函数的原型为void func(int x, int y),该函数被c编译器编译后在库中的名字为_func,而c++编译器则会产生像_func_int_int之类的名字。c++提供了c链接交换指定符号extern “C”解决库中名字匹配问题 c文件中在编译时void func(int x, int y)函数被编译后修改名字为 _func,而c++在调用时call的是_func_int_int,所以链接时,肯定会报错的
2.头文件中的ifndef/ define / endif 是干什么用的?
防止头文件被重复引用
3.#include<filename.h>和#include "filename.h"有什么区别?
第一个是从标准库目录搜索,不搜索当前目录,第二个是在当前文件路径下搜索头文件,找不到才搜索标准库目录
4.main()函数执行完毕后是否可能会再执行一段代码?
a.首先main函数也是被其余函数调用的,退出后肯定会进入其调用的函数中去,进行恢复栈空间的操作
b.在main中可以注册一个atexit()注册一个函数,这个函数会在main结束后被回调

5.const的用途
a.修饰常量 const int a = 5; 和 int const a = 5;等同编译器不为普通的const常量分配存储空间,而将他们存在常量表中,没有使得它成为一个编译期间的常量,没有了存储和读内存的操作,运行效率很高。而#define定义的宏常量有多个复制品
b.修饰函数的参数和返回值 修饰参数时,仅限于修饰按引用传的,按值传递的参数,反正已经做了拷贝本来就不需要保护,如果参数还要作为返回值,也不需要用const修饰,否则返回值无意义
c.修饰成员函数的函数体表明函数不会修改成员变量 类名::func(形参) const不能调用非const成员函数,因为任何非const成员函数都会有修改成员变量的企图
d.常引用,可以把引用绑定到const对象上。常量引用仅对引用可参与的操作进行限定,而对引用对象本身是不是一个常量未作限定,常引用常用于函数参数,当不允许函数对参数做出改变时使用。但是常对象的引用只能调用被const修饰的成员函数
e.如果函数返回值是“值传递方式”,由于函数返回时,是将返回值拷贝到外部变量,故加const没有意义,如int GetInt()改成const int GetInt()无意义
f.const修饰成员变量 成员变量必须在构造函数的初始化列表中进行初始化,或者将它设置成static类型,但是c++11可以在类声明是给定初始值

class MyClass{public:
	void  update()const {  }	//不能修改类的成员变量
	void  update(){  }	//允许这种形式的重载,非const对象优先匹配这个调用,const对象优先匹配上面那个调用
	virtual void  updateNew() {  }	//因为定义了虚函数,所以class以及每个对象内都会维护一个虚函数表的指针,所以sizeof(MyClass)结果为指针的大小 4,如果不是虚函数,则class只占用一个字节,作为占位符
};
void update(const MyClass& myclass){	//**常引用作为参数传递,可以限制函数对于对象的修改**
	myclass.update();	//在这里const 对象的引用 只能调用const成员
	//myclass.updateNew();   //const对象表明不能修改对象,而非const成员函数都有修改成员的意图
}
void  main(){
	MyClass myclass;
	myclass.update();
	myclass.updateNew();//非const对象调用const成员 没问题
	update(myclass);

在数据作为参数时,内置数据类型不需要按引用传递,因为内置数据类型不需要构造,析构,拷贝过程也很快,如fun(int a)不应修改成fun(const int& a),修改还增加了函数的理解难度。而非内置数据类型应按引用传递。

const常量有数据类型,#define宏常量无数据类型,编译时,宏只是简单的代码替换,没有类型安全检查。c++程序中只使用const常量而不是用宏常量,const常量完全取代宏常量

strlen()计算的是字符串的长度,而不是字符串占用的内存大小,所以末尾的结束符是不算在内的
strlen 是库函数,只能操作 char*
sizeof 是运算符

const变量在定义时就应该给它初始化

main()函数中
b.func0();
009870E8  mov         ecx,offset b (099236Ch)  	将 b 的地址(即this指针)传入ecx寄存器
009870ED  call        B::func0 (0981172h) 			调用func0只是调用程序代码区的 类B的func0的地址
即 call        B::func0 (0981172h),然后利用 ecx中的this指针,即程序的 数据区 b所在的地址

B::func0 () 类B的func0内的的汇编代码
class B {public:
	int bi;
	virtual void func0() { 
00906AC0  push        ebp  
00906AC1  mov         ebp,esp  
00906AC3  sub         esp,0CCh  
00906AC9  push        ebx  
00906ACA  push        esi  
00906ACB  push        edi  
00906ACC  push        ecx  								将ecx压入栈顶
00906ACD  lea         edi,[ebp-0CCh]  
00906AD3  mov         ecx,33h  
00906AD8  mov         eax,0CCCCCCCCh  
00906ADD  rep stos    dword ptr es:[edi]  
00906ADF  pop         ecx  								栈顶依然还是ecx之前的值,将栈顶再重入ecx
00906AE0  mov         dword ptr [this],ecx  			将ecx寄存器中的值取出来放到this指针中
00906AE3  mov         ecx,offset _9112CE11_testsss@cpp (0915067h)  
00906AE8  call        @__CheckForDebuggerJustMyCode@4 (09013F2h)  
		this->bi = 1;
00906AED  mov         eax,dword ptr [this]  		利用this指针取对象b中的数据,完成对对象b的操作
00906AF0  mov         dword ptr [eax+4],1  
	}

所以函数调用来说 b.func0() 等同于 B::func0(&b); 所以void func0()的定义 也可以定义为void func0(B* const _this);

函数指针 : int (* pFunc)(int ,int ) = &func1;
const 指针 const int * p = &data;
指向const的指针 int *const p = &const_data;

指针

int( * ( * F)(int,int ))(int) 。F是一个指向函数的指针,它指向这样的函数(该函数参数为int,int返回值为一个指针),返回的这个指针指向的时另外一个函数(参数为int,返回值为int类型的函数)
float( * * def)[10] 。 def是一个二级指针,它指向一个一维数组的指针数组元素是float
double * ( * gh)[10] 。gh是一个指针,它指向一个一维数组,数组元素都是double *
double( * f[10])() 。f是一个数组,有10个元素,元素都是函数的指针,指向的函数类型是没有参数且返回double的函数
int * (( * b)[10]) 。和int * ( * b)[10]一样,b是一维数组的指针
long( * fun)(int) 。函数指针

数组指针

int ( * ptr)[] 。指向整型数组的指针
int * ptr[] 。指针数组 数组里面存的是 int型指针
int * (ptr[]) 。指针数组 数组里面存的是 int型指针

删除一个空指针,不会引起程序问题

class Demo {
public:
	Demo() :str(nullptr) {};
	~Demo() { if (str)delete str; }
	//Demo(const Demo &demo){
		//if(demo.ptr){
			//this->ptr = new char[sizeof(demo.ptr)+1];
			//strcpy(this->ptr,demo.ptr);
		//}
	//}
	char *str;
};
void  main(){
	Demo d1;
	d1.str = new char[32];
	std::vector<Demo> *a1 = new std::vector<Demo>();
	a1->push_back(d1);				
	delete a1;

在这里插入图片描述
vector插入Demo的时候,会调用Demo的拷贝函数,这里不重写拷贝函数会导致拷贝的时候,只是简单的给str指针做拷贝,而不是拷贝str的内容,在执行a1析构,和d1析构会两次对str的地址做析构而出错

#include <algorithm>
#include <vector>
std::vector<int> a;
a.erase(remove(a.begin(), a.end(), 1), a.end());//批量删除容器a中等于1的元素,remove算法在algorithm文件中
一个空类通常会自动生成 
构造函数				A(){}
拷贝构造函数			A(const A& a){}
析构函数				~A(){}
赋值运算符			A& operator=(const A& a){}
取址运算符			A* operator&(){}
取址运算符const		const A* operator&()const {}

类和struct的唯一区别 类的默认成员是私有的,struct的默认成员是公有的 c++中存在struct关键字,是为了让c++编译器兼容以前c开发的项目

类的析构函数一般定义为虚函数。因为不定义为虚函数 一旦用delete 基类指针,就不会只会调用基类的析构函数,而不调用实际对象的析构函数

拷贝构造函数 是为了解决 浅拷贝 的问题

类的私有继承 class A :privite B{} .此时A的对象不能在类外部公开直接访问B类的共有函数,但是可以在A成员函数内部调用B的成员函数
继承只继承 公有函数和保护函数 (public,protected)

派生类的三种继承方式: 公有继承 ,私有继承, 保护继承,c++默认是私有继承

1.公有继承

基类的公有成员 和保护成员对派生类可见, 私有成员在派生类中不可见,它们在派生类中保持原有的对外可见性

2.私有继承

基类的公有成员和保护成员对派生类可见,基类的公有成员和保护成员都作为派生类的私有成员,私有成员不可见

3.保护继承

基类的公有成员和保护成员对派生类可见,基类的公有成员和保护成员都作为派生类的保护成员,私有成员不可见

虚继承

class A{ int a;}
class B : public virtual A{ virtual void a() {}; }; //虚继承
class C : public virtual A;
class D : public B, public C;
此时
sizeof(B) = 4(int a) + 4(虚继承的虚表指针)+ 4(虚函数的虚表)= 12 没有虚继承的话,为8
sizeof(D) = 4(int a) + 4(B的虚继承的虚表指针) + 4(B的虚函数的虚表指针) + 4(C的虚继承的虚表指针) = 16 如B,C无虚继承的话 12

c++的4种运算符转化

1.static_cast 数值转化

static_cast相当与传统C语言里的强制转换,用来强制将non-const对象转换const对象,编译时(即静态)检查用于非多态的转换,可以转换指针以及其他,但是没有运行时类型检查来保证安全性
static_cast 不能转换掉expression的 const, volatile, __unaligned的 属性

a.用于基类和派生类之间的指针或引用的转换

将派生类的指针或引用转换成基类表示,是安全的
将基类的指针或引用转换成派生类表示,是不安全的(准确说是不能),此时调用派生类的指针(如果指针指向的不是该派生类的对象)会出问题

b.用于基本数据类型的转换
c.把空指针转换成目标类型的空指针
d.把任何类型的表达式转换成void类型
2.dynamic_cast 用于执行向下转换和在继承之间的转换

在类层次间进行上行转换时,dynamic_cast 和static_cast效果一样,在下行转换时,dynamic_cast有安全类型检查,更安全。但是也特别耗性能。如果转换指针类型失败,返回nullptr,如果转换引用类型失败,将抛出std::bad_cast异常(该异常定义在typeinfo标准库头文件中)

以表达式 dynamic_cast<type *>(e)为例
a. e的类型时目标类型type的公有派生类 一定成功
b. e的类型时目标类型type的基类,当e是指针指向派生类对象,或者e是基类引用引用的是派生类对象时,转换成功,但是e指向基类的对象,转换一定失败
c.e的类型就是type 时,转换成功

3.const_cast 去掉const
void ConstTest1() {
	const int a = 1;	//定义了const常量a,它在编译时就计算完成,a为1,并且a只读取内存一次
	int *p = const_cast<int*>(&a);	//p取的是a的地址
	(*p)++;												//修改*p
	std::cout << a << std::endl;//输出1 此时a因为只读取内存一次,所以这个时候被编译器优化成输出1,而不是读内存
	std::cout << *p << std::endl;//输出2 *p因为修改过,a的内存地址对应的内存其实已经改变,但是a运行的时候,不再读取内存了
}
void ConstTest2() {
	int i = 3;
	const int a = i;//编译的时候没法确定a的具体的值,它不属于编译时常量,const只是表明它不可被直接修改,它在运行过程中会读取内存
	int *p = const_cast<int*>(&a);
	(*p)++;
	std::cout << a << std::endl;	//输出4
}
//零初始化和常量表达式初始化被称为静态初始化,而如果使用了常量表达式初始化了变量,
//且编译器根据文件内容(包含头文件)就可以计算出表达式,编译器将执行常量表达式初始化,
//必要时编译器将执行简单计算,如果没有足够信息,变量将动态初始化
const double pi = 4.0 * atan(1.0);//动态初始化,atan(1.0)计算需要调用math函数库
4.reinterpret_cast 用于执行并不安全的implementation_dependent类型转换
int main() {
	char s[] = "123456789";	
	char d[] = "123";
	strcpy(d,s);
	printf("%s\n%s", d, s);
	/*因为s[], d[]都是分配在栈内,所以栈空间 由高到低是s,d,而strcpy拷贝是以s的结束符为准,而s中包含20个字符(包括‘\0’),
	所以拷贝到d中会出现越界的情况,d向上的地址会被覆盖掉一部分,所以s中低地址的数据也会被修改。正常来说应该是s和d应该是连续
	地址,但是实际调试的时候(release留的少,debug留的多),s和d中间总是留有空闲地址。在release模式下s和d在栈中的内存布局
	(变量先后顺序会调整),会被优化,占用内存少的低地址。debug模式不会优化*/
}

一个程序中,内存分布情况
代码段:存放代码的二进制指令 ,存在硬盘中
数据段:已初始化的全局变量和已初始化的局部静态数据,const全局变量(存在只读数据段,编译期将其符号表),由已明确的初始值,数据保存在目标文件中
BBS段:存放未初始化的全局数据和未初始化的局部静态数据,目标文件值保存BBS段所含变量的名称和大小。
:由程序员分配
:由编译器自动分配并释放,存放局部变量(非静态),形式参数,函数返回值,函数调用过深,可能会导致栈溢出,const局部变量存在栈中,代码块结束就释放
数据段和BBS段 合称全局区 或者静态区
未初始化的全局变量和静态变量,编译器会给他们添加默认的初始值 0
全局变量和静态变量手动初始化为0,编译器还是按未初始化来处理的

static关键字的作用

1.修饰全局变量,表明全局变量只在本文件中可见
2.修饰局部变量,表明局部变量的生命周期为整个程序的生命周期
3.修饰函数,表明成员函数不可以访问非静态变量
4.修饰类的成员变量,表明该变量属于这个类,为所有对象共享
5.修饰类的成员函数,表明函数只能操作函数参数,类的静态成员,全局变量

int a[5] = { 1,2,3,4,5 };
int *ptr = (int *)(&a + 1);//这个是数组的偏移
&a是数组指针,其类型为int(*)[5],而指针加1 是要根据指针类型加上一定的值的
不同类型的指针 +1只后增加的大小不一,a为长度为5的int数组指针,所以要+5*sizeof(int)
所以ptr实际是a[5],但是ptr与(&a +1)类型不一样,所以ptr-1只会减去sizeof(int*)
a 和 &a 的地址一样,但是意思不一样

a是数组首地址,即a[0]的地址
&a是数组的地址
a+1是数组下一个元素的地址,即a[1]的地址
&a+1是下一个数组的地址,即 a + 5
	int a[3];// = { 1,2,3 };
	int *p = a;
	int *q = &a[2];
	q - p; //结果为2,指针一次移动一个int但是计数为1

int a[10];
sizeof(a)大小是10
数组名可以转换成为一个指向其实体的指针,而且是一个指针常量
a++;报错,提示a不是左值,数组名在函数中有常量特性,不可修改
数组名作为函数参数时,沦为普通指针,可以修改

三元表达式 a?A:B,要求表达式A和B的类型一致

void GetMemory(char **p, int num){
	*p = (char *)malloc(num);
}
int main(){
	char *str = NULL;
	GetMemory(&str, 100);//&str只是对str这个变量取地址,char *str在栈中的地址
	strcpy(str, "hello");
	free(str);	//释放str的内存,str还是指向原来的地址,但是内容已经被处理过了
	if (str != NULL)
	{
		strcpy(str, "world");
	}
	printf("\n str is %s", str);
}

编译过程分为四步:预处理,编译,汇编,链接

一.预处理

读取源代码并对其中的以#开头的指令和特殊符号进行处理。具体处理方式如下:

1.#define A a

将源代码中所有a用A代替(这里只是单纯的替换,不进行语法检查,若是复杂的代码建议加括号)

2.#include<……>

导入系统库和外部库的头文件(系统库目录下),对头文件进行展开

3.#include “……”
导入自定义的头文件(当前项目目录下)
4.处理预编译指令
a.#undef  取消已定义的宏
b.#if 如果给定条件为真,则编译下面代码
c.#ifdef 如果宏已经定义,则编译下面代码
d.#ifndef 如果宏没有定义,则编译下面代码
e.#elif 如果前面的#if给定的条件不为真,当前条件位置为真,则编译下面代码
f.#endif 结束一个#if……#else 条件编译块
5.将那些不必要的代码过滤掉,如注释
6.保留所有的#pragma编译器指令并添加行号和文件标识,这样编译时编译器就会产生调试用的行号信息,出现警告或者错误时也会显示行号
gcc -E test.c -o test.i				//c语言使用
g++ -E test.cpp -o test.i 		//c++使用

二.编译

将预处理完的文件进行词法,语法,语义分析和优化,在确认所有的指令都符合语法规则之后,将test.i翻译成汇编代码test.s。
template(函数模板和类模板)在编译阶段完成具现化。
inline在编译阶段将调用函数的代码替换为函数本体,从而减少函数调用的开销(但是一般较短且不包含switch,while等复杂结构控制语句的函数会被展开,若内联函数代码过长,inline关键字会被编译器忽略)

gcc -s test.i -O1 -o test.s
g++ -s test.i -O1 -o test.s		//-O1,-O2, -O3指编译时的优化等级,从低到高

三.汇编并生成机器码

将汇编代码test.s抓换成为二进制机器码test.o。一般认为汇编和机器码是一一对应的。但是现代汇编中的伪指令和复杂的分支循环结构不能和机器码一一对应

gcc -c test.s -o test.o
g++ -c test.s -o test.o

四.链接并生成可执行文件

链接是将多个目标文件和所需的库文件(静态库 *.a 或者 *.lib,动态库 .so或者.dll )链接成最终的可执行文件

gcc test.o -o test.out
g++ test.o -o test.out
auto

c++11新增的,能让编译器根据初始值的类型推断变量的类型

std::vector<int> scores;
auto ite = scores.begin();//ite 会自动推断为std::vector<int>::iterator

const int **pp;
const int *p0;
int *p;
p0 = p;		//可以
pp = &p;//error,只有一层间接关系时,才可以将非const地址或者指针赋给const指针
typedef const double*(* p_fun)(const double*, int);//定义p_fun为一个函数指针类型

std::string a; a.size(); //a.size()表示a对象中存储的字符串的长度

inline函数

内联函数的编译代码与其他程序代码内联起来,也就是说编译器将使用相应的函数代码替换函数调用。而对于内联代码,程序无需跳到另一个位置处执行代码,再跳回来,因此内联函数的运行速度比常规函数要稍快,但是再程序运行时会有更多的机器码加载进内存。如果程序在10个不同的地方调用同一个内联函数,则程序将包含内联函数代码的10个副本。如果执行函数代码的时间比处理函数调用机制的时间长,则节省的时间将只占整个过程的很小的一部分。如果执行时间很短,则内联调用就可以节省非内联调用使用的大部分时间
在函数声明前加上关键字inline,在函数定义前加上关键字inline。但是一般做法是省略原型,将整个定义(函数头和整个函数代码)放在原本放置 原型 的地方. c++会将函数首次使用前出现的整个函数定义充当原型定义

在声明引用时,必须对其初始化
可以通过声明来设置引用,但是不可以通过赋值来设置引用

引用(左值引用,c++11新增右值引用)

如果实际参数和引用参数不匹配,c++将生成临时变量。当前,仅当参数为const引用时,c++才允许这样做
左值:可以被引用的数据类型。如变量,数组元素,结构成员,引用和解引用的指针
非左值:字面常量(用引号括起来的字符串不算,他们由其地址表示)和包含多项式的表达式

int func(int& i);
int a = 0;  func(a);//可以
func(a+1);	//error,非常引用参数初始值必须为左值
int const_func(const int& i);
const_func(a+1);	//可以,const常引用形参,可以生成临时变量进行类型转换
const_func(1.1);	//可以,const常引用形参,可以生成临时变量进行类型转换

返回引用(要么用参数返回,要么new返回指针的拷贝)

int& func(int& i){
	i = i + 1;
	return i;
}
int a = 0;
func(a) = 2;//func返回的是a的引用,等同于a = func(a) = 2;

函数重载:函数名相同,参数列表不同 (返回值类型不同不算,因为不使用返回值的调用无法确定使用哪一个原型)

int func(int x);
int func(int &x); //两个方法在一起定义时,会报错,
//因为调用func(a)函数的时候,表达式与两个原型都匹配,没法区分,所以只能报错
void func(int &i);
void func(const int &i);
void func(int &&i);//右值引用 c++11新增的
int x = 1;
const int y = 2;
func(x);		//调用 func(int &)
func(y);		//调用 func(const int &)
func(x + y);	//调用 func(int &&),
//首先(x + y)不是左值,func(int &i)肯定不能用,但它是右值,有func(int &&i)原型,肯定优先用它
//如果没有void func(int &i),将先生成临时变量,然后强制类型转换,调用func(const int &i)
函数模板

在编译时,每个不同类型的函数模板的调用都会生成一个对应的实际函数(具现化),并连接生成对于的二进制机器码
需要对多个不同类型使用同一种算法的函数时,可以使用模板。
但是并非所有的类型都使用相同的算法,所以可以有模板的重载,调用的时候将根据实际情况使用最匹配的模板

template<typename T>void func(T a);//模板
template<typename T>void func(T* a);//模板的重载,如果调用是func(double *),它要优于上面的模板
template<> void func(double a);//模板的显式具现化,如果调用func(double),它是第二优匹配
void func(double a);//普通函数,优先级最高,如果调用func(double),它是最优匹配
template void func(int a);//显式实例化

模板并非函数定义,但使用int的模板实例是函数定义,这种实例化叫隐式实例化
一个模板不能同时存在对同一种类型的 显示实例化 和 显示具体化。
如果两个函数都完全匹配,有时候仍能完成重载解析。
指向非const数据的指针和引用要优先和 非const指针和引用 形参 匹配

double a = 0;
decltype (a) b = 1;//定义变量b,其类型与a相同
double c = 2decltype(a + c) d = a+c;//定义变量b,其类型与(a+c)的类型相同
//decltype为解决模板中T3 = T1 + T2,局部变量T3类型不明确的定义的问题
template<typename T1,typename T2>void func(T1 t1,T2 t2){
	decltype(t1+t2) T3=t1+t2;
}
template<typename T1,typename T2>
auto func(T1 t1,T2 t2) ->decltype(t1+t2)
{//因为返回值和t1+t2类型相同,如果放在前面编译器还没看到t1+t2是啥类型,所以只能放后面
	return t1+t2;//auto 只是一个占位符,表示后置返回类型提供的类型
}

类模板使用的时候,必须显式的提供所需的类型
函数模板却不需要,因为编译器可以根据函数的参数类型来确定要生成哪种函数

类模板

1.递归使用类模板
2.使用多个类型参数
3.默认类型模板参数

template<typename Type,typename T2 = int>//多个类型参数,且第二个默认是int
class Test{
private:
	template<typename V>//模板在私有部分声明,所以只能在Test类中使用它
	class hold{
		private:
			V val;
		public:
		 	hold(V v){}
	}
	hold<Type> data;
public:
	Test(Type t){}        //友元函数,是为了让外部函数能方便操作类的私有成员变量
	friend void noparam();//没有参数,友元函数不能操作对象,只能操作类静态成员,是不是类的友元函数,根本没关系
	friend void counts<Type>();//没有参数必须用<Type>指明其具体化
	friend void report<>(Test<Type>&);//有参数列表,可以省略Type只用<>来表示
};
Test<Test<int>> a;//递归使用模板类

c++11之前,register关键字表明后面的变量将存在寄存器中,c++11之后,表明变量是自动变量。这和auto之前的功能一样。现在auto为自动推导类型
static 的作用域 为当前文件
全局变量 必须符合单定义规则,即一个程序中只应存在一个同名的全局变量定义,可以用extern 定义多份声明
1.静态局部变量在代码第一次执行到变量的声明的地方是进行初始化
2.初始化过程中发生异常的话视为未完成初始化,未完成初始化的话,需要下次再执行初始化
3.再当前线程执行到需要初始化变量的时候,如果其他线程正在初始化该变量,阻塞当前线程,直到初始化完成
4.如果初始化过程中发生了对初始化的递归调用,则视为未初始化的行为

volatile 用于改善编译器的优化能力,表明变量可能会被本程序的外部环境修改,取变量值,必须读内存

struct MyStruct
{
	mutable int a;
	int b;
};
	MyStruct ss;
	const MyStruct* pss = &ss;
	pss->a = 0;//因为定义为mutable,所以允许修改
	pss->b = 0;//不可修改

全局声明的链接性是外部的(不局限于本文件)
static什么的链接性是内部的(局限于本文件)

全局const 声明的链接性是内部的

即const全局定义在头文件中,被多个文件包含是可以的,不用使用extern,但是不添加extern的时候,表明常量属于文件。即每个文件都持有自己的一组常量。如果希望全局常量的的链接性是外部的需要添加 extern 关键字来覆盖掉内部链接性,这种情况下只有在定义它的文件中可对其进行初始化

函数的链接性默认是外部的,可以在文件间共享
static关键字作用于函数原型 表明函数只能在一个文件中使用,链接性是内部的,必须同时在原型和定义都是用static,此时编译器只在该文件中找函数的定义,不加static的话,编译器包括连接器会在所有的程序文件中查找,如果程序文件中没有找到,编译器将在库中搜索。这意味着如果定义了一个与库函数同名的函数,编译器将使用程序员定义的版本
extern关键字作用于函数原型 表明函数的定义在另一个文件,链接性是外部的

要让程序再另一个文件中查找函数,该文件必须作为程序的组成部分被编译,或者是由链接程序搜索的库文件

只需要将源代码文件加入到项目中,而不用加入头文件,因为#include指令管理头文件。另外不要使用#include来包含源代码文件,这样将导致多重声明
new可能找不到请求的内存量,并且将抛出异常

void* operator new(std::size_t);     		void operator delete(void*);
void* operator new[] (std::size_t);		void operator delete[] (void*);
定位new运算符

在指定的位置new出一块内存。程序员可以使用这种特性来设置其内存管理规程,处理需要通过特定地址进行访问的硬件和特定位置创建对象

#include <new>//头文件new中包含定位new操作符的定义
char buff[500];//buff设置为内存块
int* p0 = new (buff) int;//从buff中最开始的位置取4个字节,存一个int型数据
int* p1 = new (buff + 4)int[20];//从buff+4的位置开始取20*4=80个字节存一个数组
delete p0;//将报错,delete只能用于常规new运算符分配的堆内存

namespace AAA {
	int aa = 1;
}
using AAA::aa;//将命名空间AAA中的aa变量引入到局部变量,相当于定义了一个int aa;
int aa = 3;//这时候再定义一个aa,编译的时候会报重定义的错
using namespace AAA;
aa = 4;//此时使用的是AAA::aa
定义在位于类声明中的函数都自动成为内联函数
class Test{
	Test(){}
	~Test(){}
	void do(){     }//因为直接定义在类声明中,所以默认它是内联函数
	void update();//因为这里没有定义,所以默认是普通函数
}
//但是如果在类定义处添加 inline 也可以将update变为inline函数
inline void Test::update(){          }

const成员函数不能修改对象的成员,但是不包括类的静态成员,只是不允许使用this来修改对象。等效于 const Type* this; 相当于this是常量
enum{……} 不管enum中定义了多少个类型,sizeof(enum)都为4,c++11中枚举的底层类型是整型

运算符重载

1.重载后的运算符至少有一个操作数是用户定义的类型,这防止用户为标准类型重载运算符。不能用减法运算符重载 来计算两个double值的和
2.使用运算符时不能违法运算符原来的句法规则。

  1. 不能将 运算符重载成一个操作数
  2. 不能修改运算符的优先级
    3.不能创建新的运算符,如 不能定义operator**()函数来求幂
    4.不能重载下面的运算符
  3. sizeof :sizeof运算符
  4. . :成员运算符
  5. .* :成员指针运算符
  6. :: :作用域解析运算符
  7. ?: :条件运算符
  8. typeid :一个RTTI运算符
  9. const_cast : 强制类型转换运算符
  10. dynamic_cast : 强制类型转换运算符
  11. static_cast : 强制类型转换运算符
  12. reinterpret_cast : 强制类型转换运算符
友元函数,友元类
class Type{
	int data;//私有成员
public:
	friend Type operator* (double, const Type &t);//友元函数
	friend class Test;//声明Test类是Type类的友元类
	friend void sync(Test & a,Type& b);
}
class Test{
	void func(Type& t){t.data = 0;}//因为Test是Type的友元类,所以Test中可以直接访问Type对象的私有成员
	friend void sync(Test & a,Type& b);
}
void sync(Test & a,Type& b){      }//sync 是两个类共有的友元函数

//上面因为在Test中仅仅只有func函数使用了Type类的成员,所以可以将func函数定义成Type的友元函数

class Type;//提前声明Type类
class Test{
	void func(Type& t);//{t.data = 0;}此时Type只做了声明,不能直接访问它的成员,函数定义需要放在后面
}
class Type{
	int data;//私有成员
public:
	friend Type operator* (double, const Type &t);//友元函数
	friend Test::func(Type& t);//声明Test类中的func函数是Type类的友元函数
}

也可以两者互为友元类

class Type;//提前声明Type类
class Test{
	friend class Type;//声明Type类是Test类的友元类
}
class Type{//因为互为友元类,所以两者可以互相访问他们的成员变量
	int data;//私有成员
public:
	friend Type operator* (double, const Type &t);//友元函数
	friend class Test;//声明Test类是Type类的友元类
}
虽然operator*()函数声明在类声明中,但是它不是成员函数,不能使用成员运算符来调用
虽然operator*()函数不是成员函数,但是它与成员函数的访问权限相同
class Child{
	explicit Child(int a);//在构造函数前添加explicit表明禁止隐式转换
	operator int()const{return data;}//添加这个可以允许int a = tChild;
	int data =1;//c++11中允许使用在声明中初始化,是不是const都可以
	//如果在构造child的时候,构造函数的初始化列表中有data,则不会执行data=1的操作
}
Child t = 1;//因为添加了explicit,所以这样子是不行的,如果不添加是可以的
int a = Child(1);

实际开发中,尽量避免使用隐式转换,避免添加不必要的理解难度,增加出错的可能
初始化列表中成员的初始化顺序,与成员在类声明中定义的顺序有关,与在初始化列表中的顺序无关
如果不想定义某个默认生成的构造,赋值运算符,可以显示声明为私有的

重新定义继承的方法并不是重载,如果在派生类中重新定义函数,将不是使用相同的函数参数列表覆盖基类声明,而是隐藏同名的基类方法,不管参数列表如何
1.重新定义继承的方法,应确保与原来的原型完全相同,但是如果返回类型是基类引用或指针,则可以修改为派生类的引用或指针
2.如果基类声明被重载了,则应在派生类中重新定义所有的基类版本

嵌套类

template<typename Item>//和普通类一样,模板中也可以添加嵌套类
class Test{
	private://嵌套类可以被子类继承
	class Type{//Type声明在private内,所以它只能在Test类中使用
		Item item;
	}//如果是public的,可以在类外使用,但是要加Test::作用域。Test::Type a;
}

异常

#include<exception>
class Type : public std::exception{//也可以不继承exception
	const char* what(){return "something wrong ";}
}
try{
	Type t;
	throw t;//这里定义抛出什么类型,下面就catch什么类型,不管什么类型,譬如 int
}catch(Type& e){ e.what();}//即使这里的Type定义为引用,但是使用的还是它的拷贝
//之所以定义为引用,是想要同时还捕获它派生类的异常,基类引用可以执行派生类的对象

对于new导致的内存分配问题,c++最新处理方式是让new引发bad_alloc异常。头文件new包含bad_alloc类的声明,是exception类的共有派生类
很多代码都是在new失败时返回空指针时编写的,可以

int* pi = new(std::nothrow)int;
int* pa = new(std::nothrow)int[2];//不抛出异常,返回空指针

void func() //throw (std::bad_alloc)异常规范,c++11已取消,配套unexpected函数一起使用
{ throw std::exception(); }
void terminate_handler1() {
	std::cout << "terminate_handler" << std::endl;
}
void exit_handler() {
	std::cout << "exit_handler" << std::endl;
}
int main(int argc, char** argv) {
	atexit(exit_handler);	//设置正常退出的入口,异常退出不进这个
	std::set_terminate(terminate_handler1);	//设置异常退出的入口,遇到未捕获的异常时触发
	std::set_unexcepted(unexcepted_handler);//在c++11之前和函数的异常规范配套使用,c++11之后,异常规范就没有了,在抛出的异常没在函数的异常规范中声明时,起作用
	func();
	return 0;
}
auto_ptr unique_ptr shared_ptr
std::auto_ptr<int> a(new int(0));//c++98定义的,c++11已摒弃
std::shared_ptr<int> b(new int(1));
std::unique_ptr<int> c(new int(2));
std::auto_ptr<int> a1 = a;//此时a失效,所以直接对auto_ptr赋值是不安全的,后续再调用a会出问题
std::shared_ptr<int> b1 = b;//此时b中对int指针的引用计数 加一,b或者b1需要删除的时候,计数器 减一,为0才删除
//std::unique_ptr<int> c1 = c; 不能使用已经创建的unique_ptr来给另一个unique_ptr来赋值或者初始化
std::unique_ptr<int> aa = std::unique_ptr<int>(new int(4));//但是可以使用是一个新创建的unique_ptr来做初始化
aa = std::unique_ptr<int>(new int(5));//可以使用一个新创建的unique_ptr来给另一个做赋值
aa = c;//不能使用创建过得unique_ptr来给另一个赋值

//std::auto_ptr<int []> a2  = std::auto_ptr<int[]>(new int[1]);不支持数组
//shared_ptr<int []>  c++17才开始支持
//unique_ptr<int []>支持
class Base {//也可以将两者定义为私有的
	Base(const Base&) = delete;//禁用拷贝构造函数,一般用于禁止赋值和拷贝的对象使用,譬如unique_ptr就是这样
	Base& operator=(const Base&) = delete;//禁赋值操作符,一般两者一起使用
	Base& operator++() {//for   	++t 等同于t.operator++()
		data++;
		return *this;
	}
	Base& operator++(int) {//for   t++	等同于t.operator++(0)
		Base b;
		b.data = data;
		data++;
		return b;
	}	
	int data = 0;
setlocale(LC_ALL,"chs");//将本机的语言设置为中文简体
wchar_t wt[] = L"你好";//大写L告诉编译器为里面的字节分配两个字节的空间
std::wcout<< wt << std::endl;//使用wcout来代替cout输出宽字符

cout 对于字符串数组 或者字符串指针 都输出字符串内容
对于其他的数据类型的指针,c++则将其对应于void *,并打印其地址的数值,如果相应获得字符串的地址,则必须将其转换为其他类型

ios_base::in;//打开文件,以便读取,默认是这个
ios_base::out;//打开文件,以便写入
ios_base::ate;//打开文件,并移到文件尾
ios_base::app;//追加到文件尾
ios_base::trunc;//如果文件存在,则截短文件,意味着以前的文件内容将被清空
ios_base::binary;//打开二进制文件,   正常打开文件都是读取字符内容 或者写入字符内容,想要写入或者读取二进制内容
struct Test{
	char 	str[10];
	int 	iNum;
};
Test pl;
std::ofstream fout("test.dat", std::ios_base::out | std::ios_base::binary);
fout.write((char*)&pl, sizeof pl);
std::ifstream fin("test.dat", std::ios_base::in | std::ios_base::binary);
fin.read((char*)&pl, sizeof pl);
//这种方法适用于不使用虚函数的类或者结构体,如果类有虚函数,那么虚函数表vptl的地址也将被复制进去
//而下次运行的时候,虚函数表可能在不同的位置,因此将文件中的旧指针信息复制到对象中,可能造成混乱

对于含有虚函数的类,调用虚函数的时候,不一定会通过vptl取调用虚函数

class Base{virtual void func(){}};
class Test : public Base{}//不管是否重写func
Test*p =new Test();
p->func();//通过虚函数
Test t;
t.func();//不通过虚函数,只有使用指针调用函数的时候,虚函数表才起作用
//否则在编译的时候就可以确定具体的调用函数

程序读取并显示整个文件后,将设置eofbit元素,使用clear()方法重置流状态,并打开eofbit后,程序可以再次访问该文件

c++11
新增了类型long long 和 unsigned long long,以支持64位的整型
新增了类型char16_t 和 char32_t,以支持16位和32位的字符表示
扩大了用大括号括起来的列表(std::initializer_list< T >)的适用范围
允许类的内部成员在定义的时候初始化
摒弃 export

int x = {5};
double y{2.2};
short quar[5]{2,5,4,4,1};
int *p = new int [4]{2,4,6,7};
Test pl = {"",1}; 
Test pl{"",1}; //两者都是调用Test的构造函数,而非拷贝

decltype (x) z;//将z声明为和x同一类型
enum class New1{never,sometimes,often,always};//作用域内枚举,以前同一个作用域内两个枚举不能同名
class Base{
	explicit Base(double d);//禁止 Base b = 1.0;
	explicit operator double() const;//禁止double d = b,不添加explicit可以; 必须double d = double(b);
}
for(auto item : prices){}//只要prices具有迭代器(包括指针++和其他的迭代)都可以这样取值
forward_list

单向链表,只能延一个方向遍历,与双向链表list相比,更加简单,在占用存储空间方面更经济

unordered_map

hash表实现 插入,删除,查找性能 O(1),查找性能 unordered_map > hash_map > map
元素在内部不以任何特定顺序排序,而是组织进中,每个key都会通过特定的hash运算映射到一个特定的位置,但是hash运算是可能存在冲突的(多个key映射到同一个位置),在同一个位置的元素按顺序链在后面,这个位置成为一个桶
map 是红黑树实现,有序关联容器,存储元素时,自动按从小到大的顺序排列,查找性能O(logN),中序遍历

unordered_map<int, int> mp;//max_load_factor只在reserve的时候起作用,所以要放前面
mp.max_load_factor(0.25);//管理每个桶的平均元素数量的最大值  最多1个元素4个桶,一个桶0.25个元素   设置空间的占用率
mp.reserve(1024);//在使用时候,有必要预估它的数目,避免运行时反复去申请空间,rehash
unordered_multimap

hash表实现,
键值允许重复,find的时候,返回的是key的第一个迭代器ite,当然key的第二个迭代器为 ite + 1
equal_range(key) -> 返回key所在的桶

	std::multimap<std::string, std::string> studentMap2{ {"1","a1"},{"1","a2"},{"2","a3"} };
	auto ret = studentMap2.equal_range("1");
	auto it = ret.first;
	while (it != ret.second){
		std::cout << it->first << "->" << it->second << std::endl;
		++it;
	}
unordered_set

hash表实现

unordered_multiset

hash表实现

右值引用

传统c++引用(左值引用,可以被引用的数据类型)
c++新增了右值引用,使用&&表示,
右值

可以出现在赋值表达式右边,但不能对其应用地址运算符的值。包括:
1.字面常量
2.x+y等表达式
3.函数的返回值,前提是函数返回的不是引用

引入右值主要目的之一是实现移动语义

class Base{
	Base():Base(0){}// 委托构造函数  ,避免在构造函数中有重复代码
	Base(int a);//自定义了构造函数,默认情况下编译器将不在实现默认构造函数,可以通过下面方式依旧声明默认构造函数
	//Base() = default;//强制编译器为该类生成默认的构造函数,default也可以用于其他默认的函数中
	Base(float) = delete;//强制禁用传入double或者float值,传入浮点数就会报错
	Base(Base&& d)noexcept //移动构造函数
	Base& operator=(Base&& d)noexcept //移动赋值操作符
	void func();
	void func(int);
}
class Test: public Base{
	using Base::Base;//引入Base的全部的构造函数,所以可以用
	Test(): Base(){}
	using Base::func;//使用using语句,将使下面的重定义不能屏蔽基类的func,将根据参数列表来决定具体使用的函数
	void func(int);//重定义基类中的func函数,实现屏蔽功能
}
Test t;
t.func();//调用的是Base中的func函数
t.func(1);//调用Test中的func函数
//大部分非强制移动的代码都会被编译器优化掉。如
Base a;
Base b;
Base c(a + b);//正常应该调用operator+()将a和b相加得到临时变量,然后调用移动构造
//但是实际运行时,时将a+b的结果创建的临时对象 转移到c的名下,而不调用移动构造函数

override 表明子类要重写基类的一个虚函数,用于子类
final 禁止子类重写基类的虚函数,用于基类

Lambda表达式

  • 函数指针
  • 函数对象
  • lambda表达式
class Base{
	int dv;
	bool operator()(int x){//查看x能否被dv整除
		return 0 == x % dv;
	}
};
bool func_point_div3(int x){ return 0 == x % 3;} 
std::vector<int> numbers(1000);
std::generate(numbers.begin(),numbers.end(),std::rand);//利用rand生成随机数, 函数指针
//使用函数指针
std::count_if(numbers.begin(),numbers.end(),func_point_div3);
// 也可以使用函数对象
int count = std::count_if(numbers.begin(),numbers.end(),Base(3));// 统计能被三整除的数目
//也可以用lambda表达式来统计
std::count_if(numbers.begin(), numbers.end(),[](int x)->bool{ return 0 == (x % 3);} );
//lambda表达式也可以,以便多次使用
auto lambda_div3 = [](int x)->bool{ return 0 == (x % 3);}
bool result = lambda_div3(7);//查看7是否整除3
int sum = 0;//lambda 表达式可以使用作用域内的任何变量  &sum 指 按引用访问sum
std::for_each(numbers.begin(), numbers.end(),[&sum](int x){sum += x;});//统计numbers之和
//lambda 的[] 可以传入作用域内所有的动态变量,(全局变量和静态变量不行)
//[sum] 按值访问 sum
//[&sum] 按引用访问 sum
//[&] 按引用访问 作用域内所有的动态变量
//[=] 按值访问 作用域内所有的动态变量
//[&,sum] 按值访问sum,按引用访问其他的
//[&sum, =] 按引用访问sum,按值访问其他的

一般来说,相关函数功能应该定义在一块

  • 函数指针显然不行,不能在函数调用处定义函数
  • 函数对象可以,可以在函数内部定义类,直接在调用的前面一点定义一个内部类
  • lambda可以直接定义在调用内部,而且比函数对象的实现更加简洁
    就性能而言,主要看函数是否能被内联
  • 函数指针不能被内联,编译器不会内联 其地址被捕获的函数,因为内联意味着展开,不存在函数地址
  • 函数对象和lambda 不会阻止内联
包装器wrapper

也叫适配器 adapter

  • bind 绑定函数指针,和 参数 列表
  • men_fn 能将成员函数作为常规函数进行传递传递
  • reference_wraper 创建行为像引用但可被复制的对象
  • function 能以统一的方式处理多种类似于函数的形式
class Test1 {
	int& operator()(int& x) {return x;}
};
template<class Data, class Func>
Data& use_fc(Data& t, Func fun){	return fun(t);}
int& m_test(int &d) {return d;}

int& test1(int &data, int b) {return data;}
int d = 0;
d = use_fc(d, Test());
d = use_fc(d, m_test);
//虽然调用use_fc时,Func参数都为int&,但是实际隐式实例化了两个函数模板,这个时候,可以用function包装一下,来只实例化一个函数模板
std::function<int&(int&)> fc_Test = Test();
std::function<int&(int&)> fc_m_test = m_test;
d = use_fc(d, fc_Test);
d = use_fc(d, fc_m_test);

auto f = std::bind(m_test1,d, std::placeholders::_1);//不能绑定模板函数
f(1);//data 传入的是 d,b 传入的是 1

void thread_func(int& d);
int n = 0;
std::thread t(thread_func, std::ref(n));//如果这里不用std::ref包裹住n,编译不过
t.join();
std::bind(thread_func, std::ref(n))//必须用std::ref包裹 才能绑定传引用

可变参数模板

void t_func(){	}//递归直到参数列表为空时,不在递归
template<typename T>
void t_func(const T& data) {	}//也可以递归到最后一个参数时,不在递归
template<typename T, typename... Args>
void t_func(const T& data, const Args&... args){//这里两个参数全部使用引用,防止每次展开的时候,反复拷贝
	t_func(args...);//展开参数列表
}
int main(int argc, char** argv) {
		int a = 0;int x = 1; int y = 2;
		const char* ps = "abc";
		Base b(0);
		t_func(a,x,x,y, ps, b);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值