C++学习合集

#整理到一块,方便查东西,顺便补充一些之前没有学习到的东西#

变量

char--1字节  short--2字节 int-4字节  long--4字节  long long(int)--8字节;准确来说变量的大小取决于编译器,1字节=8个二进制位,其中最高位为符号位,unsigned默认为变量为正数这时最高位就代表的数字。

char

char表示字符型变量(1字节),其存储的是对应变量的ASC码值,对这种变量赋值,其输出为对应ASC码的字符。

#include<iostream>
int main()
{
	char a='A';
	char b=65;
	std::cout<<a<<std::endl;
	std::cout<<b<<std::endl;
	std::cin.get();
	return 0;
}

输出为两个A 。

float、double

float表示含小数的变量(4字节),double也表示含小数的变量(8字节),double的精度要高于float但占用内存更多。

bool

bool表示逻辑变量(1字节),该变量=true(1)//  =false(0)

sizeof

sizeof可以查看变量的大小 。

函数

函数模块适用于重复出现的代码,保持代码的简洁。主函数是唯一的指定了返回类型但是可以不设置返回值的函数(不设置默认返回0)

用户输入

cin>>

cin>>变量,以空白(空格、制表符、换行符)来确定结束位置,将结束位置之前的输入流赋值给变量,在结尾添加空字符(\0),空白会留在之后的输入流中,这里需要注意,如果cin一开始就遇到了空白,那么cin会继续往后读取直到遇到输入流中的非空白。cin>>会返回一个cin变量,所以记住多用(cin>>变量).get();把输入流中的\n给读取了,可以避免很多难以察觉的输入错误。

cin输入类型不匹配

当变量类型为整型,但用户输入的是字符,会导致1)变量的值不会变,保持之前的值。2)不匹配的输入会留在输入流中,包括字符和换行符。3)cin对象被设置错误标记,如果之后还需要读取输入,必须用clear()重置标记才行。4)cin标记返回false。

下面这串代码最多可记录5条鱼的重量,并计算鱼的平均重量。当用户输入非数字时会退出记录部分的代码,执行计算平均重量的代码

#include<iostream>
using namespace std;
int main()
{
	const int max = 5;
	double fish[max] = {};
	int i = 0;
	double average = 0;
	cout << "fish #1:";
	while ((i < 5 ) && cin >> fish[i])
	{
		cout << "fish #" << i + 2<<':';
		
		i++;
	}
	for (int j = 0; j < i ; j++)
	{
		average += fish[j];
	}
	average /= i;
	cout << "the average fish weight is: " << average;
	if (!cin)
	{
		cin.clear();
		cin.get();//读取一个字母
	}
	cin.get();//读取一个回车
	cin.get();//程序等待用户输入回车结束。
}

cin.get()

由于C++的OOP特性(函数重载),cin.get()的形式比较多。cin.get()和cin.get(ch),ch为char类型变量,这两种都是从输出流中读取一个字符,不同的是cin.get(ch)会将读取的字符赋值给ch。

读取一行输入使用cin.get(arr,size),arr为char类型的数组名,size为整数,是将size-1大小的字符从输入流中读取,并存到arr数组中去,为什么是size-1呢?因为最后一位系统会自动补结束符(\0)。cin.get(arr,size)是用换行符来确定结束位置,cin.get()一次只读取一个但是它会读取所有输入字符,包括空格和换行符

cin.getline()

cin.getline(arr,size),也是读取一行的输入,与上面的cin.get(arr,size)相似,唯一的不同是,cin.getline会读取换行符,并且用空字符替换。

getline()

getline(cin,str),这里的str是string类的对象,这个是string类读取一行的方法,cin是参数,指出去哪里找输入,getline(cin,str)会读取换行符,并且会返回一个cin

控制流语句

continue

在循环语句中,执行到continue,continue之后的语句不再执行,进入下一次循环。

#include<iostream>

void log(const char* p)
{
	std::cout<<p<<std::endl;
}
void main()
{
	for(int i=0;i<5;i++)
	{
		continue;
		log("hello world");
	}
	std::cin.get();
}

例子中for循环遍历了五次,但是continue在函数log之前,在执行log前进入了下一次循环。

break

循环中执行到break时,直接退出循环(注意是整个循环,不是当前循环)

#include<iostream>

void log(const char* p)
{
	std::cout<<p<<std::endl;
}
void main()
{
	for(int i=0;i<5;i++)
	{
		for(int j=0;j<5;j++)
		{
		log("hello world");
		break;
		}
	}
	std::cin.get();
}

例子中,break的生命周期在内部的for循环,所以执行break时,会退出内部循环,再执行外部循环,所以本该打印25次的hello world结果只打印了5次。

return

执行return时会直接退出函数,return的应用不限制在循环中。

#include<iostream>

void log(const char* p)
{
	std::cout<<p<<std::endl;
}
int main()
{
	for(int i=0;i<5;i++)
	{
        log("hello world"); 
		for(int j=0;j<5;j++)
		{
		
		return 0;
		}
	}
	std::cin.get();
}

例子中只执行log一次,进入内部循环,执行return就会结束函数,所以只打印一次hello world,结果需要在return处打上断点进行调试,因为代码执行不到get语句。

文本文件I/O

写入文本文件

需要用到头文件#include<fstream>,必须声明一个ofstream对象,才能将文件联系起来。

#include<iostream>
#include<string>
#include<fstream>
using namespace std;

int main()
{
	ofstream outFile;
	char automobile[50];
	int year;
	double a_price;
	double n_price;

	outFile.open("carinfo.txt");
	
	cout << "Enter the make and the model of automobile: ";
	cin.getline(automobile, 50);
	cout << "Enter the model year:";
	cin >> year;
	cout << "Enter the original asking price:";
	cin >> a_price;
	n_price = a_price * 0.913;

	outFile << fixed;
	outFile.precision(4);
	outFile << "make and model:" << automobile << endl;
	outFile << "Year:" << year<<endl;
	outFile << "Was asking $" << a_price << endl;
	outFile << "Now asking $" << n_price << endl;
	outFile.close();
	return 0;

}

ofstream声明了一个对象outFile,outFile.open("文件名"),可以将文件与outFile联系起来,如果源文件夹中没有这个文件,那么编译器会在文件夹中创建一个,如果这个文件以及存在,没有其他指令下,原本这个文件中的内容会清楚后再写入新的内容。将文件与outFile联系起来之后,outFile就和cout相似,只不过cout是在控制台显示内容,而outFile是在文本文件中显示内容(输入文本文件中)。outFile<<fixed是设置浮点数输出格式为固定点方式,不然可能用科学计数法来表示。outFile.precision(4)是设置浮点数精度为4位。

读取文本文件

需要用到头文件#include<fstream>,必须声明一个ifstream对象,才能将文件联系起来。

#include<iostream>
#include<string>
#include<fstream>
using namespace std;
int main()
{
	ifstream inFile;
	inFile.open("carinfo.txt");
	while (inFile.is_open()) 
	{
		string A;
		getline(inFile, A);
		cout << A<<endl;
		while (inFile.eof())
		{
			cout << "end of file\n";
			cin.get();
			return 0;
		}
	}
		cout << "file dosen't exist\n";
	cin.get();
}

 ifstream声明了一个对象inFile,inFile.open()将文件与inFile联系起来,inFile与cin相似,只不过cin是将控制台的输入赋值给变量,而outFile是在文本文件中的内容赋值给变量,inFile.is_open()是检查文件是否被打开,是返回true,否返回false;inFile.eof()是检查是否到文件末尾。

getline(inFile,A)的返回值是inFile本身,inFile可转化为bool值,读取输入成功为true,失败为false,所以代码可以写成下面这种形式。

#include<iostream>
#include<string>
#include<fstream>
using namespace std;
int main()
{
	ifstream inFile;
	inFile.open("carinfo.txt");
	while (inFile.is_open()) 
	{
		string A;
		if (getline(inFile, A))
			cout << A << endl;
		else {
			cout << "end of file\n";
			cin.get();
			return 0;
		}
	}
		cout << "file dosen't exist\n";
	cin.get();
}

实际上输入失败,不止有文件末尾的情况,还有类型不匹配的可能(可用方法fail()来判断),或者文件或硬件故障(可用方法bad()来判断), 类型不匹配暂时不知道是什么情况下会产生。

指针

指针即地址,用于记录变量或函数在内存中所在位置。在c与c++中,指针的类型需要与要储存的变量类型一致,如int* 只能储存int类型变量的地址(void* 例外,这种类型的指针可以存任何类型变量的地址)。

解引用

常见的解引用符号有*,[  他们是对指针所指的变量内容进行操作。

#include<iostream>
void main()
{
	int* p=NULL;
	int a=1;
	p=&a;
	*p=2;
	std::cout<<"a="<<a<<std::endl;
	std::cin.get();
}

由例子可知,对*p的赋值直接导致变量a的数值改变。赋值操作不会改变a的地址,所以在代码执行的过程中p的值没有改变(p的值就是a的地址)。

前面提到的void*可以储存任何变量类型的地址,但是这种类型的指针在解引用赋值时需要将指针类型强制转换后才可进行。

引用

引用实际上就是让变量有了第二个变量名,引用的类型需要与变量的类型相同。

#include<iostream>
void main()
{
	int a=1;
	int& ref=a;
	ref=2;
	std::cout<<"a="<<a<<std::endl;
	std::cin.get();
}

要注意这里的int&中的&是类型的一部分,并不是取地址操作。由代码可知对ref的赋值操作,直接改变了变量a的值,并且查看内存可知a和ref的地址相同,所以这两个变量其实是一个变量,只占一个整型变量的大小。

如果要在一个函数中实现主函数中的变量改变,容易想到的一种是传入变量的地址,然后进行解引用操作。因为a和ref地址相同,所以函数形参用引用类型接收,也可以实现主函数变量的改变。

#include<iostream>
void selfincre(int& ref)
{
	ref++;
}
void main()
{
	int a=1;
	selfincre(a);
	std::cout<<"a="<<a<<std::endl;
	std::cin.get();
}

如果形参使用int去接收,那么在运行函数的过程中,函数会创建临时变量对临时变量(与a的地址不同)进行自加操作后退出函数即销毁,无法实现变量a的改变,当然对于占用内存大的变量比如整型数组,传地址才是是更高效的操作。

c++中的类与c中的结构体类似,都是为了方便变量的管理,c++中通过class创新的变量类型。

class playerinfo
{
public:
     int x,y;
     int speed;
};

创建的末尾记得加分号。public意味着可以在类之外的任何地方访问类中的变量,如果c++中不声明则默认是private,在main函数中无法访问,这也是类与结构体的区别,类中的变量如果不声明默认private,在其他函数中无法调用,而结构体内的变量默认public。

方法

类中内置的函数称为方法,处于类之外的函数要对类中的变量,需要传入类的指针,而方法位于类的内部,则不需要传指针。

#include<iostream>
using namespace std;
class playerinfo
{
	char name;

     int x,y;
     int speed;
public:
	 void move(int a,int b)
	{
		x=a;
		y=b;
	}
};

void main()
{
	playerinfo p;
	p.move(2,3);
	cin.get();

}

调试过程中,在内存中查看p.x,p.y可知,方法move改变了类中private的部分。

静态

在同一项目下两个.cpp文件中,如果全局变量中出现同一个变量名,系统就会报错,这时将其中一个文件中的变量定义前加上static,这时就可以通过编译。使用static后这个变量只会在翻译单元的作用域内寻找这个变量的定义,所以不会发生重定义。

如果想使用另一个文件下的全局变量,可使用extern,这时连接器就会在作用域外去寻找这个变量的定义。

结构体中的静态

static可以让结构体(或者类)中的变量变成只有一个实例,也就是不管构建几个结构体变量,其中的成员(本例中的x)都指向唯一地址。

#include<iostream>
using namespace std;
struct entity
{
	static int x;
	int y;
	void print()
	{
		cout<<x<<","<<y<<endl;
	}
};
int entity::x;

void main()
{
	entity i;
	entity p;
	p.x=1;
	p.y=2;
	i.x=3;
	i.y=4;
	p.print();
	i.print();
	cin.get();
}

注意需要对静态成员x进行声明,否则会报错。静态变量的调用可以使用 entity::x

静态的方法不能访问静态的变量。

静态

局部静态

在讲局部静态前,我们要了解变量的生命周期,在一个函数内的定义变量称为局部变量,这个变量的生命周期仅在函数执行期间,函数执行结束后这个变量就会被销毁。而函数内部的静态变量会一直存在于程序的整个生命周期中,程序结束时才会被销毁,静态变量只会初始化一次。

#include<iostream>
using namespace std;
void add()
{
	static int p=0;
	p+=1;
	cout<<p<<endl;
}
void main()
{
	int i=0;
	add();
	add();
	cin.get();
}

如果去掉static 两次打印的结果均为1,这里虽然p在函数结束后没有被销毁,因为p的生命周期是从第一次定义开始到程序结束,所以打印结果为1和2;但是在第二个add处添加断点进行调试,内存中却看不到p的地址(希望有大佬能解释一下为什么看不到地址)。

枚举

枚举是将一组整数值集合作为类型,第一个数默认为0,往后依次递增1。

#include<iostream>
using namespace std;

enum example: unsigned char
{
	A,B,C=3,d
};

void main()
{
	cout<<A<<endl;
	cout<<B<<endl;
	cout<<C<<endl;
	cout<<d<<endl;
	cin.get();
}

如果变量有赋值,该变量值改变,往后按改变后的值依次递增1,所以输出0,1,3,4 。枚举类型默认整型,如果变量数值不大就没有必要用32位的整型,将其改为8位整型可以节省内存。

构造函数

构造函数是一种特殊类型的方法,每次构造一个对象时都会被调用,它没有返回值类型,其名必须与类的名称相同,如果不实例化对象构造函数就不会运行。构造函数的特性对于初始化来说比较方便。

#include<iostream>
using namespace std;
class player
{
public:
	float x,y;
	
	player(float i, float j )
	{
		x=i;
		y=j;
	}
	player()
	{
		x=0;
		y=0;
	}
	void print()
	{
		cout<<x<<endl;
		cout<<y<<endl;
	}
};
void main()
{
	player p1(1.0f,1.0f);
	player p2;
	p1.print();
	p2.print();
	cin.get();
}

一个类中可以有多个构造函数,但多个构造函数名称相同,所以必须保证参数不同才能区分不同的构造函数,例子中p1调用了第一个构造函数,p2调用第二个。

析构函数

析构函数是在销毁对象时运行,用于清理使用过的内存。在构造函数中进行了数据初始化,如果不清理可能会导致内存泄漏。~类名称即为解析函数。

继承

两个不同的类,但是有许多相同的公共部分(变量,函数),就可以通过继承去减少代码的重复。

比如:游戏中的子弹,需要有位置信息,有移动;对于玩家同样需要位置信息,需要可以移动,还需要游戏id。

class cartridge
{
public:
	float x,y;
	void move(float i,float j)
	{
		x+=i;
		y+=j;
	}
};
class player:public cartridge
{
	const char* name;
};

player中就包含了cartridge中的变量以及函数。

虚函数

在子类中可以重写基类的方法,让该方法做其他的事情。注意virtual需要写在基类需要重写的方法前面,override可以不写(virtual跟override可二选一写上),写上只是为了预防差错(如果有拼写错误,vs会提示而不是直接创建一个新的方法)

#include<iostream>
#include<string>
using namespace std;
class entity
{
public:
	virtual string getname()
	{
		return "entity";
	}
};
class player:public entity
{
private:
	string m_name;
public:
	player(const string& name)
	{
		m_name=name;
	}
	string getname()override
	{
		return m_name;
	}
};
void main()
{
	entity* e=new entity() ;
	player* p=new player("lee");
	cout<<e->getname()<<endl;
	cout<<p->getname()<<endl;
	entity* entity=p;
	cout<<entity->getname()<<endl;
	cin.get();
}

std::string可以理解为字符串类型名,在创建player的实例时要注意构造函数的参数在创建时就必须传入。new可以理解为malloc,会创建一个它后面跟着的那个类型的内存空间并返回相应的指针类型,这里内存块虽然是在堆上创建的但是指针是在栈上创建,指针在程序运行到作用域外就被销毁。这里输出entity lee lee。如果不使用虚函数这里就会输出entity lee entity。

这里做一个讨论,为什么这里要用指针呢?可以看以下代码。

#include<iostream>
#include<string>
using namespace std;
class entity
{
public:
	virtual string getname()
	{
		return "entity";
	}
};
class player:public entity
{
private:
	string m_name;
public:
	player(const string& name)
	{
		m_name=name;
	}
	string getname()
	{
		return m_name;
	}
};
void main()
{
	entity e;
	player p("lee");
	cout<<e.getname()<<endl;
	cout<<p.getname()<<endl;
    entity en=p;
	cout<<en.getname()<<endl;
	cin.get();
}

代码中使用了虚函数,我们将p赋值给en,期望输出跟之前代码一样的结果entity lee lee。但是结果是entity lee entity。这里对en进行赋值时发生了对象切片(派生类象赋值给基类对象时,只会复制基类对象中的成员,而派生类特有的成员信息会丢失。)也就是p的getname并没有赋给en。但是这个赋值如果是用的引用(entity& en=p)那就可以得到预期的结果,很神奇。

纯虚函数

在基类中定义一个没有实现的函数,然后强制子类去实现该函数。纯虚函数是虚函数=0的形式创建。

#include<iostream>
#include<string>
using namespace std;
class entity
{
public:
	virtual string getname()=0;
};
class player:public entity
{
private:
	string m_name;
public:
	player(const string& name)
	{
		m_name=name;
	}
	string getname()override
	{
		return m_name;
	}
};
void main()
{
	entity* e=new player("lee") ;
	cout<<e->getname()<<endl;
	cin.get();
}

这里需要注意几点:1)无法创建有纯虚函数的实例,所以new 后面只能是entity的子类player。2)如果注释掉子类的getname函数,子类实例也无法创建,说明只能实现纯虚函数之后才能实例化。(与虚函数不同,就算重载的函数被注释掉,基类与子类的实体依然可以创建。)

但是说实话,不太明白这样做有什么意义。

可见性

可见性会影响变量的调用,c++中有三种基础的可见性修饰符public protected private。之前学习过在结构体中不声明可见性的默认为public,在类中不声明可见性的默认为private。private变量只能在定义内进行调用,也就是说在基类定义的private变量,不管是在main函数中还是在其子类中都无法调用。protected变量能在定义内以及其子类中进行调用,但在其他地方无法调用。public变量的调用则没有限制。

数组

数组就是多个同种类型的变量的集合,数组的定义方法是:类型+数组名+[元素个数]。

void main()
{
	int arry[5]={};
	cout<<arry[0]<<endl;

	*(arry+3)=5;
	cout<<arry[3]<<endl;
	cout<<sizeof(arry)<<endl;
	cout<<sizeof(&arry)<<endl;

	*((char*)arry+3)=17;
	cin.get();
}

注意:1)大括号里面不输入任何值可以把所有元素初始化为0。2)数组名arry是整型指针,arry+3是偏移了三个整型的大小。3)虽然数组名是地址,但是用sizeof计算大小时,得到的是整个数组的大小。4)强制转换arry使指针可以一个字节一个字节的操作,将arry第一个元素值改为0x11000000可在内存中查看。

字符串

字符串其实就是字符数组,字符串的定义方法可以用两种,字符类型指针和字符类型数组。

#include<iostream>
using namespace std;
void main()
{
	char* name="lee";
	cout<<name<<endl;

	char name2[3]={'l','e','o'};
	cout<<name2<<endl;

    char name3[]="lioness";
    cout<<name3<<endl;
	cin.get();
}

需要注意几点:1)c++中char*定义的字符串默认不可修改,也就是默认加了const(只能读取,不能复写),当使用name[2]='o';试图修改第三个字符调试时会报错。2)name2的打印结果中除了前三个leo外还会出现一些奇怪的字符,这是由于字符串的结束的判断是由ASC码值为0的NUL(写作'\0'或者数字0)决定的,name2中三个字符之后的内容没有初始化,所以直到遇见0才会结束输出。3)name2的字符数组初始化方法过于麻烦,所以引入name3,中括号中的元素个数可以不填,编译器会自动计算,并且不用加\0,编译器会自动补上。4)注意区分字符串常量与字符常量,例如"s"与's'是不同的,"s"包含的是字符s和\0并且表示的是首元素地址,而's'表示的是其对应的ASC码值83。

std::string

这里了解一下符合C++的字符串用法std::string。

#include<iostream>
#include<string>
using namespace std;
void main()
{
	string str1="goo";
	string str2="good";
	str1=str2;
	cout<<str1<<endl;

	string str3="game";
	str2+=str3;
	cout<<str2<<endl;

    cout<<str2[2]<<endl;
	cin.get();

}

相比于字符串数组,string类的字符串变量的处理更接近于普通变量,字符串数组并不能直接相互赋值,而string类不仅可以相互赋值还能自动调整字符串的长度,所以不存在溢出的问题。并且string类的字符串合并操作更简便,本例中就将str3接在了str2后。

string类依然可以像char数组那样通过[下标]来访问对应位置的字符。

 mutable

mutable是C++中的关键字,可以类中与常量方法配合使用,可以在lambda表达式中使用。

#include<iostream>
#include<string>
using namespace std;
class X
{
private:
	int m_x ,m_y;
	mutable int i;
public:
	
	int getx()const
{
	i=0;
	return m_x;
}
};
void func(const X&x)
{
	cout<<x.getx()<<endl;
}
void main()
{}

这里func的形参调用的是常量引用,则调用的方法getx也必须是常量,const加在括号之后(只有类的方法可以这样写)表示的不是不能改变成员变量,也就是说在getx中对m_x赋值是非法的。而mutable使得可以在常量方法getx中修改变量i。

类中的成员初始化

类中成员初始化一般有两种方法,一是在构造函数中进行赋值,二是在构造函数的参数之后添加冒号对,变量以及赋值进行说明。

#include<iostream>
#include<string>
using namespace std;
class entity
{
private:
	string m_name;
	int index;
public:
	entity(string& name)//删除&
	{
		m_name=name;
		index=0;
	}
	entity()
		:m_name("lee"),	index(1)
	{}
	void printname()
	{
		cout<<m_name<<endl;
	}
};
void main()
{
	string name="odd";//string可改为char*
	entity e1(name);
	e1.printname();

	entity e2;
	e2.printname();
	cin.get();
}

在打代码的时候发现有个有趣的事,std::string本身是一个类,所以当构造函数形参接收类型为string&时只能接收string类型的变量,但是当引用符号去除之后,构造函数则可以接收char*类型的参数传入(也就是可以传入地址)。

在第二种初始化的方法上稍加变化。

class entity
{
private:
	string m_name;
	int index;
public:
	entity(string& name)
		:m_name(name),index(1)	
	{}
	void printname()
	{
		cout<<m_name<<endl;
	}
};

初始化方法更推荐使用第二种,因为第一种在某些情况下会造成性能损失。

#include<iostream>
#include<string>
using namespace std;
class example
{
public:
	example()
	{
		cout<<"creat entity"<<endl;
	}
	example(int x)
	{
		cout<<"creat entity"<<x<<endl;
	}
};
class entity
{
private:
	example m_exam;
public:
	entity()
	{
		m_exam=example(8);
	}
};
void main()
{
	entity e1;
	cin.get();
}

创建e1时产生了两个example类,一个在成员定义处,调用了第一个构造函数,一个在example(8),调用了第二个构造函数,我们期望的是直接创建一个example(8)的实例,所以第一个创建其实是多余的。使用第二种初始化的定义方法就可避免这种浪费。

三元运算符

三元运算符就是分号和问号,可以简化条件语句的书写。

#include<iostream>
using namespace std;
void main()
{
	int i;
	cin>>i;
	cin.get();
	if(i>10)
		cout<<"level"<<i<<endl;
	else if(i>5)
		cout<<"lev"<<i<<endl;
	else
		cout<<"l"<<i<<endl;

	
	i>5?i>10?cout<<"level"<<i<<endl:cout<<"lev"<<i<<endl:cout<<"l"<<i<<endl;
	cin.get();
}

下面这个三元运算符的嵌套与上面的if-elseif的功能相同。

NEW

new的主要目的是在堆上分配连续的内存块,延长变量的生命周期,但是相比在栈上创建的变量会更慢。其返回值是指向该分配的内存块的指针。

class player
{
private:
	string s;
	int a;
public:
	player():s("name")
	{
		cout<<s<<endl;
	}
};
void main()
{
	cout<<sizeof(string)<<endl;
	cout<<sizeof(player)<<endl;

	int* b=new int[20];
	player* p=new player;
	player* e=(player*)malloc(sizeof(player));

	delete p;
	delete[] b;
	free(e);
	cin.get();
}

这里做一些讨论:1)string类的大小为32字节,int类型4字节,player类36字节,可知其中的默认构造函数不占内存。2)new与malloc的用法类似,但是malloc只是为e开创了36字节的内存空间并返回地址,new不仅开辟空间还调用了默认构造函数。3)用了new之后要记得使用delete去删除堆上分配的内存空间,否则会造成内存泄漏。4)new int[20]是创建了80字节的整型数组空间,在删除的时候比较特殊delete[] 变量名。

隐式转换(implicit)

这里通过一个例子来展示。

#include<iostream>
#include<string>
using namespace std;
class entity
{
private:
	string m_name;
	int m_age;
public:
	entity(const string& name)
		:m_name(name),m_age(-1){}
	entity(int age)
		:m_name("unknown"),m_age(age){}
};
void print(const entity& e)
{}
void main()
{
	entity a="lee";
	entity b=2;

	print(4);
	print((string)"leo");
	cin.get();
}

这里让a等于字符串,让b等于一个整型,看起来有些奇怪,这就是隐式转换或者隐式构造函数,这里隐式地将"lee"转成了entity("lee"),隐式转换只能发生一次,这里留个坑,这里"leo"无法直接隐式转化成entity类必须先将"leo"强转成string类后才能通过一次隐式转换变成entity类,为什么"lee"就可以直接转换?

explicit

如果explicit放在构造函数前面,就意味着没有隐式转换。添加explicit之后就无法像上例那样创建实例了。

运算符重载

在C++中如果我们想实现两个结构体中对应地成员相加,结构体变量直接相是非法的,我们可以通过运算符重载来实现。

#include<iostream>
#include<string>
using namespace std;
struct player
{
	float x,y;
	player(float x,float y)
		:x(x),y(y){}
	player operator+(const player& other)const
	{
		return player(x+other.x,y+other.y);
	}
	player operator*(const player& other)const
	{
		return player(x*other.x,y*other.y);
	}
};
void main()
{
	player p1(4.0,4.0);
	player p2(0.5,1.5);
	player p3(1.1,1.1);

	player result1=p1+p2;
	cout<<result1.x<<endl;
	cout<<result1.y<<endl;

	player result2=p1*p2;
	cout<<result2.x<<endl;
	cout<<result2.y<<endl;

	player result3=p1+p2*p3;
	cout<<result3.x<<endl;
	cout<<result3.y<<endl;
	cin.get();
}

重载的参数括号外的const是为了确保对象成员不会被修改,事实上符号重载其实和函数没有什么区别,我们也可以通过定义两个函数来实现加和乘的功能。

这里演示一下在类之外进行运算符重载。

#include<iostream>
#include<string>
#include<memory>
using namespace std;
class entity
{
private:
	char* m_name;
	int m_size;
public:
	entity(char* s)
	{
		m_size=strlen(s);
		m_name=new char(m_size+1);
		memcpy(m_name,s,m_size+1);
	}
	friend std::ostream& operator<<(std::ostream& i,const entity& str);
};
std::ostream& operator<<(std::ostream& i,const entity& str)
{
	i<<str.m_name;
	return i;
}
void main()
{
	entity s("odd");
	cout<<s<<endl;
	cin.get();
}

这里重载了左移操作符<<,左移操作符位于ostream类中,与直接在类里边重载相比,在类外重定义它时需要在括号中加一个操作符所属的类的引用,即std::ostream& i,这个i可以理解为形参,你可以自由设定。

对于字符串的复制当然是使用string更方便,但是如果是传入的char*,则不能用string去接收(因为类型不匹配),而且不管是strcpy还是memcpy都不支持string类型,因为memcpy是按m_size的值复制相应的长度的字符串到m_name中,而strlen并不会计算结束符(\0)在内,所以copy长度要加一,strcpy则不需要,它会自动在复制的最后加上\0。

this

通过关键字this,可以访问成员函数(方法),this是一个指向当前实例的指针。

#include<iostream>
#include<string>
using namespace std;
class entity
{
public:
	int x,y;
	float i,j;
	entity(int x, int y)
	{
		x=x;
		y=y;
	}
	entity(float i,float j)
	{
		this->i=i;
		this->j=j;
	}
};
void main()
{
	entity e1(1,2);
	cout<<e1.x<<endl;
	cout<<e1.y<<endl;

	entity e2(1.2f,3.4);
	cout<<e2.i<<endl;
	cout<<e2.j<<endl;
	cin.get();
}

在第一个构造函数处形参的变量名和我们想修改的成员变量名相同,这里编译器执行的其实是将形参的值赋给形参,成员变量仍是随机值,第二个构造函数处使用了this指代当前实例的指针,实现了成员变量的初始化。

智能指针

智能指针能自动实现前面所讲的new分配内存和delete删除内存。

unique_ptr

unique_ptr是作用域指针,超出作用域时它会被销毁,然后调用delete。

#include<iostream>
#include<string>
#include<memory>
using namespace std;
class entity
{
public:
	entity()
	{
		cout<<"created entty"<<endl;
	}
	~entity()
	{
		cout<<"distroyed entty"<<endl;
	}
	void print(){}
};
void main()
{
	{
		unique_ptr<entity>e(new entity());
		e->print();

        unique_ptr<entity>e0=e;//错误语法
	}
	cin.get();
}

这里由析构函数可知在智能指针e超出作用域时被自动删除了,因为e是指针,在调用内部成员时y要用->。错误语法是想说明unique_ptr是不能复制的(因为是unique)。

实际上这里用unique_ptr<entity>e=make_unique<entity>();创建更好,不会因为构造函数异常得到一个没有引用的悬空指针,从而造成内存泄漏。但是由于我用的VS2010并没有这个函数模板。

share_point

share_point通过引用计数的方法,追踪指针有多少个引用,当引用技术达到0,内存块删除。

#include<iostream>
#include<string>
#include<memory>
using namespace std;
class entity
{
public:
	entity()
	{
		cout<<"created entty"<<endl;
	}
	~entity()
	{
		cout<<"distroyed entty"<<endl;
	}
	void print(){}
};
void main()
{
	{
		shared_ptr<entity>Se0;
	{
		shared_ptr<entity>Se=make_shared<entity>();
		Se0=Se;

	}
	}

	cin.get();
}

使用make_shared函数的原因则与make_unique不同,share_point需要分配一块内存用于储存引用计数,如果先使用new再传递给share_point的话就必须做两次内存分配,make_shared则将其组合起来,更加高效。通过调试可知,在第一个左括号处Se的作用域结束,但entity并没有被删除,在第二个左括号处,Se0作用域结束,entity被删除,调用析构函数。

weak_point

weak_point 可以接受share_point并且不会引起计数器的增加。

#include<iostream>
#include<string>
#include<memory>
using namespace std;
class entity
{
public:
	entity()
	{
		cout<<"created entty"<<endl;
	}
	~entity()
	{
		cout<<"distroyed entty"<<endl;
	}
	void print(){}
};
void main()
{
	{
		weak_ptr<entity>Se0;
	{
		shared_ptr<entity>Se=make_shared<entity>();
		Se0=Se;

	}
	}

	cin.get();
}

在第一个左括号处Se的作用域结束,entity就会被删除,Se0指向一个无效的entity。

拷贝

拷贝就是将一个对象的数据或者内存复制到另一个地方,但是拷贝需要时间,如果只是想读取数据则应该避免没必要的拷贝。

浅拷贝

#include<iostream>
#include<string>
#include<memory>
using namespace std;
class entity
{
private:
	char* m_name;
	int m_size;
public:
	entity(char* s)
	{
		m_size=strlen(s);
		m_name=new char(m_size+1);
		memcpy(m_name,s,m_size+1);
	}
	~entity()
	{
		delete m_name;
	}
	friend std::ostream& operator<<(std::ostream& i,const entity& str);
};
std::ostream& operator<<(std::ostream& i,const entity& str)
{
	i<<str.m_name;
	return i;
}
void main()
{
	entity s("odd");
	entity i=s;
	cout<<s<<endl;
	cout<<i<<endl;
	cin.get();
}

entity i=s; 这个语句就是浅拷贝,使用的是C++中提供的默认拷贝函数 entity(const entity& other)

:m_name(other.m_name),m_size(other.m_size){},如果将拷贝函数删除(entity(const entity& other)=delete;),那么entity i=s;将会报错,这也是unique_ptr的原理。

这串代码可以正常运行,但在键入enter,退出程序时会报错,原因是s复制给i这条语句,i 和s是两个变量,位于不同的位置,在C++中执行的是将m_name和m_size的值复制到了i的内存地址处,两个实例其中的m_name指向了同一块地址,在程序结束时析构函数启动,导致同一个地址被delete了两次。(留个坑,个人认为实例 i 中的默认构造函数没有调用,如果有调用那么new重新分配内存的话就不会报错)

这里代码其实有错误,new char(4)是分配一个字节的内存并将asc值为4的字符存入该地址,然后返回该地址,实际上应该是new char[4],这个才是分配四字节的内存,用错误的代码调试时虽然编译器没有报错,但是其实已经非法访问了。

深拷贝

利用默认拷贝函数的框架,我们可以构造自己的拷贝函数,实现m_name的内存在拷贝时重新分配,程序结束时就不会报错了,实现代码如下。

#include<iostream>
#include<string>
#include<memory>
using namespace std;
class entity
{
private:
	char* m_name;
	int m_size;
public:
	entity(char* s)
	{
		m_size=strlen(s);
		m_name=new char[m_size+1];
		memcpy(m_name,s,m_size+1);
	}
	entity(const entity& other)
		:m_size(other.m_size)
	{
		m_name=new char[m_size+1];
		memcpy(m_name,other.m_name,m_size+1);
	}
	~entity()
	{
		delete[] m_name;
	}
	friend std::ostream& operator<<(std::ostream& i,const entity& str);
};
std::ostream& operator<<(std::ostream& i,const entity& str)
{
	i<<str.m_name;
	return i;
}
void main()
{
	entity s("odd");
	entity i=s;
	cout<<s<<endl;
	cout<<i<<endl;
	cin.get();
}

函数传参(直接传变量)的时候也会发生复制,拖慢程序,将形参设置为引用变量就可以避免在传参时发生复制。传参要习惯用const!!!

自制智能指针

#include<iostream>
#include<string>
#include<memory>
using namespace std;
class entity
{
public:
	void demonstr () const
	{
		cout<<"hello"<<endl;
	}
};
class scopedptr
{
private:
	entity* m_obj;
public:
	scopedptr(entity* const e)
		:m_obj(e){}
	~scopedptr()
	{
		delete m_obj;
	}
	const entity* operator->()const
	{
		return m_obj;
	}
};
void main()
{
	const scopedptr e=new entity();//scopedptr e(new entity())
	e->demonstr();
	cin.get();
}

智能指针要求在超出作用域时被自动删除,scopedptr实现了这一功能,接下来做几点说明:1) const scopedptr e=new entity() 这句等效于 scopedptr e(new entity()) 是将 new entity 返回的指针作为构造函数的输入,并不是隐式转换!!2)如果没有箭头操作符的重载,那么 e 是没有办法像指针那样通过箭头符号去调用方法,因为 e 本质上是 scopedptr 类的实例。3)因为操作符重载返回值为 const entity,如果方法不加const是无法调用的。

计算偏移量

#include<iostream>
using namespace std;
struct vector3
{
	float x,y,z;
};
int main()
{
	int offset1=(int)&((vector3*)0)->x;
	cout<<offset1<<endl;

	int offset2=(int)&((vector3*)0)->y;
	cout<<offset2<<endl;

	cin.get();
}

(vector*)0 是将0地址强转为vector类型指针,&((vector*)0)->x是取出这个0指针下x的地址也就是偏移量。

Vector

vector本质上是一个动态数组而不是向量,vector使用new和delete来管理内存,这两种操作是自动的,使用时可以设定一个大小进行初始化,但如果是要创建确定大小的数组用普通的数组定义更佳,因为vector效率较低,你使用初始设定了一个大小的 vector ,但是输入的数据超过设定的大小时,vector会创建一个比之前数组更大的新数组,并把所有东西复制到新数组,然后删除旧数组。其定义为vector<类型名>变量名(元素个数),(元素个数)这一项可以省略。

#include<iostream>
#include<vector>
using namespace std;

struct Vertex3
{
	float x,y,z;
	Vertex3(float x,float y,float z)//VS2010不支持初始化列表的语法,只能手动使用构造函数
		:x(x),y(y),z(z){}
};
std::ostream& operator<<(std::ostream& stream, const Vertex3&v)//方便打印
{
	stream<<v.x<<","<<v.y<<","<<v.z;
	return stream;
}
int main()
{
	vector<Vertex3>vertices;
	vertices.push_back(Vertex3(1,2,3));
	vertices.push_back(Vertex3(4,5,6));

	for(int i=0;i<vertices.size();i++)
		cout<<vertices[i]<<endl;

	vertices.erase(vertices.begin()+1);//删除第二个元素,erase的参数要求输入迭代器所以不能直接输入整数比如2之类的
	for(int i=0;i<vertices.size();i++)
		cout<<vertices[i]<<endl;

	cin.get();
}

这里进行一些说明,push_back() 是添加元素,如果是使用的版本稍微新一点的vs,Vertex3的构造函数可以删除,添加元素的语句可以简化为 vertices.push_back({1,2,3});  vs2010不支持初始化列表的语法,说真的,要不是我c盘容量告急,真的不想用vs2010。重载了左移操作符方便打印,记得多用引用,避免传参时发生数据复制,拖慢程序运行速度。vertices.size()可返回vertices数组中元素个数。erase可删除对应位置的元素,但是erase的参数要求输入迭代器所以不能直接输入整数比如2之类的。

Vector使用优化

使用vector导致代码运行变慢的主要原因是容量不够大,需要容纳多余的元素就得重新分配内存,并把之前的内存复制到新的内存中去,再删除旧内存,复制的过程尤其花费时间。

#include<iostream>
#include<vector>
using namespace std;
struct Vertex
{
	float x,y,z;
	Vertex(float x,float y,float z)
		:x(x),y(y),z(z){}
	Vertex(const Vertex& e)
	{
		cout<<"copy"<<endl;
	}
};
void main()
{
	vector<Vertex> vertices;
	vertices.push_back(Vertex(1,2,3));
	vertices.push_back(Vertex(4,5,6));
	vertices.push_back(Vertex(7,8,9));

	cin.get();
}

在添加第一个元素处添加断点进行调试,我们发现在添加第一个元素时就发生了复制,原因是当我们创建vertices实际上驶在主函数的栈上创建的(成员为1,2,3的那个元素),但是我们要把vertices放入vector所创建的内存中,这里从主函数复制到vector类中发生了一次复制。最终这段代码发生了6次复制。

优化一

预设容量,如果已知容量是3。

#include<iostream>
#include<vector>
using namespace std;
struct Vertex
{
	float x,y,z;
	Vertex(float x,float y,float z)
		:x(x),y(y),z(z){}
	Vertex(const Vertex& e)
	{
		cout<<"copy"<<endl;
	}
};
void main()
{
	vector<Vertex> vertices;//vector<Vertex> vertices(3)
	vertices.reserve(3);
	vertices.push_back(Vertex(1,2,3));
	vertices.push_back(Vertex(4,5,6));
	vertices.push_back(Vertex(7,8,9));

	cin.get();
}

不能使用vector<Vertex> vertices(3),这串代码不仅仅是分配了3个Vertex的内存还构建了三个对象,但是在这一步我们并不想在这里创建对象,reserve可以确保我们有足够的内存,这里我们将copy次数减少到了三次。

优化二

使用 emplace_back 而不是 push_back ,emplace_back只传递构造函数的参数列表,然后vector内存中使用这些参数构造一个 Vertex 对象,省去了复制的开销。

#include<iostream>
#include<vector>
using namespace std;
struct Vertex
{
	float x,y,z;
	Vertex(float x,float y,float z)
		:x(x),y(y),z(z){}
	Vertex(const Vertex& e)
	{
		cout<<"copy"<<endl;
	}
};
void main()
{
	vector<Vertex> vertices;
	vertices.reserve(3);
	vertices.emplace_back(1,2,3);
	vertices.emplace_back(4,5,6);
	vertices.emplace_back(7,8,9);

	cin.get();
}

这里能将copy次数降到0,但是VS2010不支持,运行时会报错。

库通常包含两个部分,包含目录(includes)和库目录(library),包含目录是一堆头文件,这样就可以使用预构建的二进制文件中的函数;库目录中就有那些预构建的二进制文件中的函数。

静态库

静态库是在编译时将库的代码和数据直接链接到目标程序中,生成一个包含所有代码和数据的可执行文件(.exe),这是装入时链接。静态链接在技术上更快,但是会导致源码体积变大,并且一旦发生修改就要重新编译。

这里说一下我遇到的一些问题,一开始,我按以下步骤设置好后,依旧调试报错,无法解析外部函数glfwInit等等一堆erro,我以为是编译器版本跟静态库不匹配(我用的VS2010但是能下载的GLFW库最早版本是2013的),之后我换了VS2022也使用了相应的GLFW库但是依然报错,这时VS报错中提示了解决方案平台的问题,要链接到这个库,解决方案平台需设置成x86。

首先在解决方案目录下创建一个名为dependencies的文件夹。

在dependencies文件夹中创建一个GLFW文件夹,并将下载的库中的include和lib文件夹复制到GLFW文件夹中,lib的选择应该于编译器版本相同或者接近。

右键点击helloworld项目,点击属性,在C/C++下的常规中设置附加包含项目,这个地方要设置include这个文件夹所在位置,路径可以使用宏$(SolutionDir),这个表示的是解决方案的路径。然后点击确定。

之后便可以在项目中引用头文件#include"GLFW\glfw3.h", glfw3.h中是一大堆函数的声明,但是想要使用还需要函数的定义。

右键点击helloworld项目,点击属性,找到链接器下的常规,在附加库目录中输入glfw3.lib的路径。

在输入里添加附加依赖项glfw3.lib

设置完毕后这串代码即可通过调试。

#include<iostream>
#include"GLFW/glfw3.h"
int main()
{
	int a = glfwInit();
	std::cout << a;
	std::cin.get();
}

动态库

动态库是在运行时被链接的,你可以在选择程序运行时装载动态库;或者在程序启动时加载dll文件,这是动态链接库。

操作大部分都与链接静态库相同,这里不再赘述,只将一些不同的部分展示出来。

项目属性->链接器->输入->附加依赖项->将glfw3.lib替换成glfw3dll.lib

复制glfw3.dll到可执行文件(HelloWorld.exe)所在的文件夹中。

这段代码可以运行,说明我们成功链接到了动态库。

#include<iostream>
#include"GLFW/glfw3.h"
int main()
{
	int a = glfwInit();
	std::cout << a;
	std::cin.get();
}

 创建与使用库(多项目)

右键点击解决方案选择添加,可以在原有的基础上添加新的项目。Game将成为可执行文件,在其属性中设置常规属性->配置类型为应用程序(.exe);Engine要静态链接,在其属性中 1.平台设置为所有平台 2.设置常规属性->配置类型为静态库(.lib)。

在Game项目下新建一个src文件夹,在里面添加Application.cpp

在Engine项目下新建一个src文件夹,在里面添加Engine.cpp,Engine.h  设置好后测试一下是否可用。

Engine.h

namespace engine {
	void PrintMessage();
}

Engine.cpp

#include<iostream>
#include"Engine.h"

namespace engine {
	void PrintMessage() 
	{
		std::cout << "Hello World!" << std::endl;
	}
}

Application.cpp

#include"Engine.h"
#include<iostream>
int main()
{
	engine::PrintMessage();
	std::cin.get();
}

在Application.cpp中无法直接引用Engine.h,因为他们不在一个项目中。点击Game项目的属性-> C/C++ ->附加包含目录,添加engine源目录。

 虽然头文件已经包含进来了,但是我们还没有链接到静态库,Application还无法运行,右键点击Engine项目选择生成,在输出中可以看到生成的Engine.lib的路径。

要静态链接到Engine.lib可以参考静态库里提到的步骤,这里尝试使用一下添加引用,右键点击Game->添加->引用->项目->勾选上Engine->确定。这样就完成了静态链接,Application.cpp已经可以运行了。

添加引用的好处: 1)如果Engine改名成其他,不需要重新在项目属性里设置输入文件。2)Engine内部如果发生变化比如修改代码之类的,不需要手动去重新生成Engine.lib。

 多返回值的处理

如果返回值是同一种类型,那么可以返回vector或者数组,但是C++中不能返两种类型的返回值,在实际情况下,一个函数可能就是需要返回一个字符串和一个浮点型,这时应该如何处理呢。

返回结构体

将函数的返回值设置为一个结构体,结构体成员中添加我们需要返回的几种变量类型。

	#include<iostream>
	using namespace std;
	struct A 
	{
		string str;
		float i;
	};
	A function(string name,float attrib)
	{
		return {name,attrib};
	}

	int main()
	{
		A player1;
		player1=function("odd", 1.0);
		cout << player1.i << endl;
		cout << player1.str << endl;
		cin.get();
	}

这里要注意返回值不能是结构体指针,因为函数内部创建的变量会在函数结束时销毁,如果用指针去接收,那么player1与player指向同一地址,但是 player.str , player.i 的值就已经失效了。

返回元组

元组是一个类,它可以包含多个变量且不限制类型。

	#include<iostream>
	#include<tuple>
	using namespace std;
    tuple<string, float>function(string str,float i)
	{
		return make_tuple(str, i);
	}
	int main()
	{
		auto i= function("odd", 1);
		cout << get<0>(i) << endl;//元组的成员访问
		cout << get<1>(i) << endl;
		cin.get();
	}

其实返回元组与返回结构体类似 ,但是对于元组成员的访问并不像结构体成员的访问那样清晰,结构体成员的访问带上了变量名,在篇幅长的代码中能够提示我们变量代表的含义,所以返回元组使用的并不多,这里也仅作一个了解。

结构化绑定

在之前的多返回值处理我们提到了返回元组与返回结构体类似 ,但是对于元组成员的访问并不像结构体成员的访问那样清晰。在C++17中提出的结构化绑定,让元组成员的访问变得更清晰。

#include<iostream>
#include<string>
#include<tuple>
std::tuple<std::string, int> CreatPerson()
{
	return{ "odd",24 };
}
int main()
{
	auto [name, age] = CreatPerson();
	std::cout << name << std::endl;
	std::cout << age << std::endl;
	std::cin.get();
}

如果编译有问题就右键点击项目->属性->c/c++->语言->语言标准->C++17。这样可以命名返回的元组变量的成员,并且如果这个返回类型只用一次,那么为了这一次的使用去创建结构体就显得有些不值。

引用传参

用引用传入函数的参数,可以达到在函数内部对函数外部变量的修改,这时就可以取消返回值。

#include<iostream>
using namespace std;	
void function(string& str, float& i)
	{
		str = "odd";
		i = 1;
	}
	int main()
	{
		string str1;
		float i = 0;
		function(str1, i);
		cout << str1 << endl;
		cout << i << endl;
		cin.get();
	}

指针传参

使用指针传参应对多返回值的原理与使用引用传参相同,都是达到函数内部对函数外部变量的修改的效果,从而不需要返回值。

模板

模板解决代码复用

我们看一个简单的例子。

	#include<iostream>
	#include<string>
	using namespace std;
	void print(string str)
	{
		cout << str << endl;
	}
	void print(int str)
	{
		cout << str << endl;
	}
	void print(float str)
	{
		cout << str << endl;
	}
	int main()
	{
		print("odd");
		print(1);
		print(3.5f);
	}

要打印不同的类型,就要传入不同的参数,可以用函数重载实现,但是这样复制粘贴的代码看起来很臃肿,像这种几个代码相同,只是传入的参数不同的函数就可以用模板来实现。

#include<iostream>
#include<string>
using namespace std;
template<typename T>
void print(T value)
{
	cout << value << endl;
}
int main()
{
	print("odd");//print<string>("odd")
	print(1);
	print(3.5f);
	cin.get();
}

void print(T value)  只是模板并不是真正的函数,当我们在main函数中调用print时,函数才真正创建。

模板生成类

#include<iostream>
#include<string>
using namespace std;

template<typename T,int N>
class Array
{
private:
	T m_Array[N];
public:
	int getsize()const { return N ; }
};
int main()
{
	Array<int,5> array;//创建一个Array类,所有的T替换成int,所有的N替换成5
	cin.get();
}

如果不使用模板,那么在栈上创建的数组在编译期间就必须知道它的大小,而使用模板在调用时才会创建。

内存分配

#include<iostream>
#include<string>
using namespace std;
struct Vector
{
	int x, y, z;
	Vector()
		:x(10),y(11),z(12) {}
};

int main()
{
	//栈上分配
	int value = 5;
	int array[5] = { 1,2,3,4,5 };
	Vector v1;

	//堆上分配
	int* hvalue = new int(5);
	int* harray = new int[5];
	harray[0] = 1;
	harray[1] = 2;
	harray[2] = 3;
	harray[3] = 4;
	harray[4] = 5;
	Vector* hv1 = new Vector();

	cin.get();
	delete hvalue, hv1;
	delete[] harray;
}

第一个部分是在栈上分配内存,第二个部分是在堆上分配内存(用关键字 new ),打上断点进行调试,在内存中可以看见value、array、v1 的位置其实挨得很近,他们之间的字节( cc )是在debug模式运行下添加的安全守卫,确保变量溢出时不会影响到其他变量。

在栈上分配内存时,实际上是栈顶部的指针移动变量所需字节大小, 我用的VS2022,变量是从低地址向高地址分配的,其他版本可能不同。

在堆上分配则上面这些特征。new 实际上是调用了 malloc 。简单来说,当启动程序时我们会得到一定数量的物理ram;程序会维护一个叫空闲列表的东西,用于追踪那些内存块是空闲的,位置在哪以及被分配的情况。

在代码生成的汇编中我们可以看出,栈上分配只是一条简单的cpu指令,堆上分配则多出了很多语句,所以栈上分配的速度要快许多,实际上,不是必须在堆上分配(变量需要超出函数作用域的寿命,或者要分配的内存很大),采用栈上分配是最好的。

当我们编译C++文件时,首先预处理器会过一遍所有带# 的语句(预编译指令符号),然后进行文本替换。

#include<iostream>
using namespace std;
#define  SQUARE(X) X*X

int main()
{
	int a = SQUARE(5);
	cout << a << endl;

	int b = SQUARE(2 + 3);
	cout << b << endl;

	cin.get();
}

这里写了一个宏来实现数的平方,注意X是参数符号标记并不是真正的传参,宏虽然看起来像函数但并不是传递参数来实现的,只是简单的文本替换,b=2+3*2+3,所以a,b的结果不相同。

宏并不适合拿来写函数,容易发生一些不容易察觉的错误,比如上面代码中b的结果,更麻烦的是宏是在预处理阶段进行的文本替换,并不能添加断点调试,这就导致宏引发的错误更难以发现。

在宏定义中不一定所有语句都得写在同一行,可以使用 \来实现多行的书写。反斜杠后面一定不能有空格!!!(有空格就是对空格的转义而不是对换行的)

类型别名

类型别名可以使用宏和typedef实现。类型别名就是可以用自己定义的符号来表示类型,通常在类型名较长时使用。下面是一个简单实例。

#include<iostream>
#define Pfloat float*
typedef float* dfloat;
int main()
{
	Pfloat A, B;
	dfloat C, D;
	
	cin.get();
}

宏定义实现的类型别名是别名在前,原类型名在后;而typedef则相反,并且别忘记分号。宏定义实现的类型别名在声明一系列变量时会出问题,这里A是float* 类型,但是B只是float类型,尽量避免这种问题。typedef不会出现这种问题。

auto

#include<iostream>
int main()
{
	auto a = 5;
	auto b = "odd";
	auto c =5.0f ;
	cin.get();
}

从这段代码可以看出,我们不需要特别关注类型,auto可以让变量自动对应上其赋值的类型。当变量类型比较简单时用auto会导致代码可读性下降,所以auto一般都运用在类型名过长(无须映射)和不知道类型的情景下。

既然auto可以根据输入的类型自定义的改变参数类型,那么它是否可以用在函数的形参,实现像模板那样的函数复写功能呢?这是不行的!!!在C++中,函数参数需要明确指定类型,以便编译器进行类型检查和错误处理,确保正确的行为。

array

array是静态数组,与vector不同array不能改变设定的元素个数,语法为 std::array<类型,元素个数>数组名,可以像访问数组那样访问它,array与c语言中的数组差别不大,都是储存在栈上。

#include<iostream>
#include<string>
#include<array>
using namespace std;
int main()
{
	array<int, 5>data;
	data[0] = 0;

	int dataold[5];
	dataold[0] = 0;

	cin.get();
}

相比于c风格的数组,array增加了一层调试,可检查数组是否越界,可记录数组大小,没有性能损失。

 函数指针

原始函数指针

原始函数指针来自C语言,函数的函数名就是一个原始函数指针,它代表的是函数所在的地址。

#include<iostream>
#include<string>
using namespace std;
void HelloWorld(int i)
{
	cout << "hello world!" << endl;
}
int main()
{

    void(* func1)(int)  = HelloWorld;
	func1(1);

	auto func2 = HelloWorld;
	func2(2);

	cin.get();
}

函数指针就是函数的指针,首先它是指针所以要具有指针的形式 * func1(func1是变量名),其次要有函数的形式,且类型要相同所以有 void( )(int)。使用auto接收,书写比较简单。

#include<iostream>
#include<string>
#include<vector>
using namespace std;
void print(int a)
{
	cout << a << endl;
}

void foreach(const vector<int>& A,void(* P_func)(int))
{
	for (int a : A)
		P_func(a);
}

int main()
{
	vector<int> A = { 1,5,3,2,4 };
	foreach(A,print);	
	cin.get();
}

上面这串代码主要是想介绍一下for(int a:A)这条语句,在每次循环中,将容器 A 中的元素赋值给变量 a。它用于遍历容器中的元素,普通的数组无法使用这条语句;如果想通过a改变A中元素的值,那么声明处需要用 int& a

lambda表达式

lambda 表达式是一种定义匿名函数的方式,这种函数是一次性的。

#include<iostream>
#include<string>
#include<vector>
using namespace std;
void foreach(const vector<int>& A, void(*P_func)(int))
{
	for (int a : A)
		P_func(a);
}

int main()
{
	vector<int> A = { 1,5,3,2,4 };
	foreach(A, [](int value) {cout << value << endl; });
	cin.get();
}

这串代码与之前的代码功能相同,只是print函数,我们使用lambda表达式简化了。

[](int value) {cout << value << endl; } ,这里定义了我们的lambda函数,void(*P_func)(int) 这个函数指针限定了我们的lambda要写成什么样子(void类型,所以lambda中没有返回值;传参类型为int,所以lambda参数列表有int value)

[]中括号内的内容是捕获,可以不捕获任何东西,也可以捕获多个,捕获就是传入函数外部的变量,假如我们在main函数中定义了一个int i=0;我们如何把i传入lambda中,首先,lambda是在P_func(a)这里被调用的,所以可以先把 i 传入 foreach,再传入P_func,对应的要改变参数列表。或者我们直接捕获i [i],这样书写后foreach的参数列表需要修改一下,因为非捕获lambda可以隐式转换成函数指针,而有捕获的不行,所以不能用void(*P_func)(int)接收。这里接收的形参涉及头文件#include<functional> 暂时还没学,留作补充。

namespace

名称空间主要目的是避免命名冲突,假如我们有两个名字同为 print 的函数,传入参数都是char*,一个函数功能是打印字符串,另一个函数功能是反转后打印。C中的做法是改变函数名称,一个用print,另一个用reverse_print,C++中的做法就是使用不同的空间名称(这里因为形参类型相同所以不能用函数重载)。

#include<xutility>
#include<iostream>
#include<string>
using namespace std;
namespace apple {
	void print(const char* text)
	{
		cout << text << endl;
	}
}
namespace orange {
	void print(const char* text)
	{
		string temp = text;
		reverse(temp.begin(),temp.end());
		cout << temp << endl;
	}
}

int main()
{
	const char* A = "abcd";
	apple::print(A);
	orange::print(A);

	cin.get();
}

线程

我们之前所有的代码都是单线程的,意思是我们只让计算机一次做一行代码或一条指令,每当代码中有cin之类的命令,程序运行到此处就会等待用户输入,这时候程序就像被阻塞了,不会继续运行。当代码体积很大时,某些待执行语句并不需要用户此次的输入,这种等待就会很浪费时间。

#include<thread>
#include<iostream>
using namespace std;

static bool s_finished = false;
void Dowork()//工作线程中执行的内容
{
	
	while (!s_finished)
	{
		
		cout << "working\n";
		
	}
}

int  main()
{
	thread worker(Dowork);//同时执行工作线程
	cin.get();
	s_finished = true;

	worker.join();//工作线程结束后再进行下面的主线程

	cin.get();
}

这里通过两个线程(主线程和工作线程)实现了,不断打印内容的同时等待用户输入回车来终止打印,这是单靠函数无法实现的,因为在单一线程中会在cin.get()处阻塞。这里我尝试将worker.jion()注释掉,在我按了回车之后依旧输出了一行working才停下并且在调试结束时发生了错误abort() has been called,我查了一下说是同步的问题,当主线程改变s_finished时,工作线程并没有立即看到新的值。所以我在注释掉这条语句的情况下,在工作线程语句中添加了sleep_for,让循环慢下来,这次按下回车后没有多余的working被打印,可是依旧报错。这里我感觉应该不是工作线程没有看到s_finished的改变,因为没有多余的输出,这里留个坑,想不到合理的解释,因为worker.join()的作用也只是等待worker线程结束再进行它下面的主线程的语句,然而多线程本来就可以并行运行,总不可能是worker还没结束运行就运行主线程导致的错误吧。

计时

计时可以计算出执行代码时花费了多长时间,对于同样的目的,我们可以通过不同的代码实现,而执行时间长短是评价一串代码性能如何的指标。

#include<iostream>
#include<string>
#include<chrono>
#include<thread>
using namespace std;

int main()
{
	
	auto start=chrono::high_resolution_clock::now();//这个是当前时间  now函数的返回类型std::chrono::steady_clock::time_point
	this_thread::sleep_for(1s);
	auto end = chrono::high_resolution_clock::now();

	std::chrono::duration<float> duration = end - start;
	cout << duration.count() << ' s' << endl;

}

chrono::high_resolution_clock::now();这个是记录当前时间,now函数的返回类型std::chrono::steady_clock::time_point,类型名过长所以用auto,std::chrono::duration<float>设置了结束和开始时间之差的结果float类型,但是这个结果并不能直接用float类型去接收。

#include<iostream>
#include<chrono>
using namespace std;
struct Timer
{
	std::chrono::steady_clock::time_point start, end;
	std::chrono::duration<float> duration;
	Timer()
	{
		start = chrono::high_resolution_clock::now();
	}
	~Timer()
	{
		end = chrono::high_resolution_clock::now();
		duration = (end - start);
		float ms = duration.count() * 1000.0f;
		cout << ms << "ms " << endl;
	}
};

void func()
{
	Timer timer;
	for (int i = 0; i < 100; i++)
	{
		cout << "Hello!\n";
	}
}

int main()
{
	func();
	cin.get();
}

Timer结构体运用了构造函数和析构函数的特性,在创建实例时使用构造函数,在生命周期结束时执行析构函数,只需要在函数开始时创建一个结构体实例,就能计算函数的执行时间,有多个需要计算时间的函数时,使用 Timer 就不需要重复写那几行代码了。

多维数组

多维数组如二维、三维等,在C和C++中的使用都不太方便,内存分配比较复杂,这里只做简单介绍。

#include<iostream>
using namespace std;
int main()
{
	
	int** a2d = new int* [5];
	for (int i = 0; i < 5; i++)
		a2d[i] = new int[5];
	for (int i = 0; i < 5; i++)
		delete[] a2d[i];
	delete[] a2d;
	cin.get();
}

一个n维数组的数组名就相当于一个n级指针,所以在堆上分配二维数组时需要分配两次,第一次分配各一级指针的内存,第二次分配各元素的内存。要回收二维数组的内存也需要迭代,如果先delete[] a2d,会导致找不到各元素的位置,无法使用delete[] a2d[i]可能导致内存泄漏。这里25个元素,我们创建了5个单独的缓冲区,每个缓冲区五个元素,但是缓冲区的位置在内存中是完全随机的,可能很近也可能很远,当我们对元素进行遍历时,必须在内存中跳转到另一个位置来读写数据,这会导致缓存不命中(cache miss),花费大量时间在ram中读取数据,如果是紧密分配在一起的就不会有这样的问题。所以比起多维数组,不如建立一个内存空间大一点的一维数组,一维数组也不需要分配额外的空间来记录5个单独的缓冲区的首元素地址。

排序

std::sort 是C++内置的排序函数,使用sort 要引入头文件algorithm ,sort有三个参数,1.排序起始位置,2.排序结束位置,这两个位置都是需要用迭代器,不能用普通数组只能用模板类,3.排序方式,这个参数可以不填,如果不填默认是升序排序。

#include<iostream>
#include<vector>
#include<algorithm>
#include<functional>
using namespace std;
int main()
{
	vector<int> values = { 3,2,1,5,4 };
	sort(values.begin(), values.end());//默认升序
	for (int value : values)
		cout << value << ',';
	cout<<endl;

	sort(values.begin(), values.end(),greater<int>());//降序
	for (int value : values)
		cout << value << ',';
	cout << endl;

	sort(values.begin(), values.end(), [](int a, int b) {return (a < b); });
	for (int value : values)
		cout << value << ',';

	cin.get();
}

排序方式可以传入模板类,也可以传函数,a和b,如果你先让a排前面就返回true否则返回false。

return (a<b), a<b为真,a就排前面,所以是升序排列,如果是return (a>b),a>b为真,a排前面所以为降序,这也是greater的原理。

union

共用体和结构体相似,但是共用体一次只能占用一个成员的内存,所有成员共用同一地址。

#include<iostream>
using namespace std;
union A
{
	int int_val;
	float float_val;
}a;
int main()
{
	a.float_val = 2.0f;
	cout << a.float_val << endl;
	cout << a.int_val << endl;
	cin.get();	
}

这里a.int_val并不等于2,因为int_val与float_val地址相同,int_val的值其实是float_val在内存中的值。我们可以通过下面这串代码来理解。

#include<iostream>
using namespace std;
int main()
{	
	float b = 2.0;
	int a=*(int*) & b;
	cout << a << endl;


	a = b;
	cout << a << endl;
	cin.get();
}

第一个部分,int a=*(int*) & b ,是取b的地址然后强转为整型指针,再解引用赋值给a,和int_val与float_val的情况类似;而第二个部分,存在隐式转换,实际上是 a=(int )b ;这个部分的结果a=2 。

虚析构函数

虚析构函数并不是在子类中复写基类的析构函数,更像是在基类析构函数的基础上加一个析构函数。

#include<iostream>
using namespace std;
class Base
{
public:
	Base() { cout << "Base construct\n"; }
	~Base() { cout << "Base distroy\n"; }
};
class Derive:public Base
{
public:
	Derive() { cout << "Derive construct\n"; }
	~Derive() { cout << "Derive distroy\n"; }
};
int main()
{
	Base* base = new Base();
	delete base;
	cout << "------------\n";
	Derive* derive = new Derive();
	delete derive;
	cout << "------------\n";
	Base* poly = new Derive();
	delete poly;

	cin.get();
}

在删除poly时并没有调用Derive的析构函数,这里可能会导致内存泄漏,改进方法就是使用虚析构函数。 在基类的虚构函数名前加上virtual即可。只要一个类拥有子类,那么它的析构函数就必须是虚函数否则无法保证安全的扩展这个类。

类型转换

C++中用于类型转换的有static_cast ,reinterpret_cast,dynamic_cast,以及 const_cast,相比于C风格的类型转换,这些语句增加了一些检查功能,各自的使用也有一些限制,下面进行一些简单的介绍。

#include<iostream>
using namespace std;
int main()
{
	double a = 5.25;
	double b = (int)a + 5.3;
	cout << b << endl;
	b = static_cast<int>(a) + 5.3;
	cout << b << endl;
	cout << "------------------\n";

	int c = 1;
	b = *(double*) &c;
	cout << b << endl;
	b = *reinterpret_cast<double*>(&c);
	cout << b << endl;
	cout << "------------------\n";


	cin.get();
}

static_cast只能实现一些基本类型的转换比如float,int之类的,无法实现整型地址向double类型指针的转换,可以看到C风格的转换也可以实现这些功能,如果对性能有要求用C风格是最好的。

#include<iostream>
using namespace std;
class Base
{
public:
	virtual void prt() { cout << "Base type\n"; }
	virtual ~Base() { cout << "Base distroy\n"; }

};
class Derive:public Base
{
public:
	void prt()override  { cout << "Derive type\n"; }
	~Derive()  { cout << "Derive distroy\n"; }
};
int main()
{
	Base* b1 = new Base();//指向父类的父类指针
	Base* b2 = new Derive();//指向子类的父类指针

	Derive* derive = dynamic_cast<Derive*>(b1);
	if (!derive)
		cout << "transmition fail\n";
	else
		derive->prt();
	derive = dynamic_cast<Derive*>(b2);
	if (!derive)
		cout << "transmition fail\n";
	else
		derive->prt();

	delete (b1,b2);//错误的,只删除了b2
	cin.get();
}

dynamic_cast用于有继承关系的转换,转换不成功会返回null,转换不成功的情况:1)两个没有继承关系的类指针之间的转换。2)下行转换(父转子)的情况中,指向父类的父类指针转换为子类指针会失败,而指向子类的父类指针可以转换为子类指针。(如果有多个子类,只能转换成指向的子类)3)上行转换不会失败。4)delete (b1,b2)涉及逗号表达式,逗号表达式的值是右值,所以只删除了b2。如果是delete b1,b2 那么就只删除b1因为逗号的优先级很低,相当于(delete b1),b2 这里b2不起作用。

  • 45
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值