C++学习8-C++提高编程

Posted mw_1422102031

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++学习8-C++提高编程相关的知识,希望对你有一定的参考价值。

文章目录


前言

只是为方便学习,不做其他用途,原作者为黑马程序员
B站配套视频:
https://www.bilibili.com/video/BV1et411b73Z

本阶段主要针对C++泛型编程和STL技术做详细讲解,探讨C++更深层次的使用

一、模板

1.1 模板的概念

模板就会通用的模具,大大提高复用性。

例如生活中的一寸照片、PPT模板。


模板特点:

  • 模板不可以直接使用,它只是一个框架
  • 模板的通用并不是万能的

1.2 函数模板

  • C++另一种编程思想称为泛型编程,主要利用的技术就是模板
  • C++提供两种模板机制,函数模板和类模板。

1.2.1 函数模板语法

函数模板作用 :
建立一个通用的函数,其函数返回值类型和形参类型可以不具体制定,用一个虚拟的类型来代表。

语法 :

template<typename T>
函数声明或定义

解释:
template——声明创建模板
typena me——表明其后面的 符号为一种数据类型,可以用class代替。
T——通用的数据类型,名称可以替换,通常为大写字母。

#include<iostream>
using namespace std;


//函数模板
//两个整型交换
void  SwapInt(int& a, int& b)

	int temp = b;
	b = a;
	a = temp;

//两个浮点型交换
void SwapDouble(double& a, double& b)

	double temp = a;
	a = b;
	b = temp;



//函数模板
//声明一个模板,告诉编译器后面的代码中紧跟着的T不要报错,T是一个通用数据类型
template<typename T>
void MySwap(T& a, T& b)

	T Temp = a;
	a = b;
	b = Temp;


void test01()

	int a = 10;
	int b = 20;
	//利用函数模板进行交换
	//1.自动类型推导
	MySwap(a, b);

	cout << a << endl;
	cout << b << endl;

	double c = 11.1;
	double d = 12.2;
	//2.显示指定类型
	MySwap<double>(c, d);
	cout << c << endl;
	cout << d << endl;

int main(void)

	test01();
	system("pause");
	return 0;


1.2.2 函数模板注意事项

注意事项:

  • 自动类型推导,必须推导出一致的数据类型T,才可以使用
  • 模板必须要确定出T的数据类型,才可以使用

示例:

#include<iostream>
using namespace std;

//利用模板提供通用的交换函数
template<class T>
void mySwap(T& a, T& b)

	T temp = a;
	a = b;
	b = temp;



// 1、自动类型推导,必须推导出一致的数据类型T,才可以使用
void test01()

	int a = 10;
	int b = 20;
	char c = 'c';

	mySwap(a, b); // 正确,可以推导出一致的T
	//mySwap(a, c); // 错误,推导不出一致的T类型



// 2、模板必须要确定出T的数据类型,才可以使用
template<class T>
void func()

	cout << "func 调用" << endl;


void test02()

	//func(); //错误,模板不能独立使用,必须确定出T的类型
	func<int>(); //利用显示指定类型的方式,给T一个类型,才可以使用该模板


int main() 

	test01();
	test02();

	system("pause");

	return 0;


总结:使用模板时必须确定出通用数据类型T,并且能够推导出一致的类型

1.2.3 函数模板案例

案例描述:

  • 利用函数模板封装一个排序的函数,可以对不同数据类型数组进行排序
  • 排序规则从大到小,排序算法为选择排序
  • 分别利用char数组和int数组进行测试

示例:

#include<iostream>
using namespace std;
//实现通用 对数组进行排序的函数
//规则 从大到小
//算法 选择 
//测试 char 数组 int 数组
//交换的函数模板
template<class T>
void mySwap(T& a, T& b)

	T temp = a;
	a = b;
	b = temp;


template<class T>
void mySort(T arr[], int len)

	for (int i = 0; i < len; i++)
	
		int max = i;//认定最大值的下标
		for (int j = i + 1; j < len; j++)
		
			//认定的最大值比遍历出的数值要小,说明j下标的元素才是真正的最大值
			if (arr[max] < arr[j])
			
				max = j;
			
		
		if (max != i)
		
			//交换max和i元素
			mySwap(arr[max], arr[i]);
		
	


//打印数组模板
template<class T>
void myPrint(T arr[], int len)

	for (int i = 0; i < len; i++)
	
		cout << arr[i] << " ";
	
	cout << endl;


void test01()

	char charArr[] = "badcfe";
	int num = sizeof(charArr) / sizeof(char);
	mySort(charArr, num);
	myPrint(charArr, num);

void test02()

	int intArr[] =  2,3,5,9,7,1,4,8,6 ;
	int num = sizeof(intArr) / sizeof(int);
	mySort(intArr, num);
	myPrint(intArr, num);

int main(void)

	test01();
	test02();

	return 0;

复习:计算数组长度

1.2.4 普通函数与函数模板的区别

普通函数与函数模板区别:

  • 普通函数调用时可以发生自动类型转换(隐式类型转换)
  • 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
  • 如果利用显示指定类型的方式,可以发生隐式类型转换

隐式类型转换的理解:

#include<iostream>
using namespace std;
//普通函数与函数模板的区别
//普通函数调用可以发生隐式类型转换
//函数模板用自动类型推导不可以发生隐式类型转换
// 函数模板用显式指定类型 可以发生隐式类型转换

//普通函数隐式类型转换
int myAdd01(int a, int b)

	return a + b;


//函数模板
template<class T>
T myAdd02(T a, T b)

	return a + b;


void test01()

	int a = 10;
	int b = 20;
	char c = 'c';  //正确,将char类型的'c'隐式转换为int类型  'c' 对应 ASCII码 99
	cout << myAdd01(a, c) << endl;

	//自动类型推导不行
	//cout << myAdd02(a, c) << endl;

	//显式指定类型行
	cout << myAdd02<int>(a, c) << endl;



int main(void)

	test01();
	system("pause");
	return 0;

总结:建议使用显式指定类型的方式,调用函数模板,因为可以自己确定通用类型T

1.2.5 普通函数与函数模板的调用规则

调用规则如下:

  1. 如果函数模板和普通函数都可以实现,优先调用普通函数
  2. 可以通过空模板参数列表来强制调用函数模板
  3. 函数模板也可以发生重载
  4. 如果函数模板可以产生更好的匹配,优先调用函数模板

#include<iostream>
using namespace std;

//普通函数与函数模板调用规则
void myPrint(int a, int b)

	cout << "调用的普通函数" << endl;


template<typename T>
void myPrint(T a, T b)

	cout << "调用的模板" << endl;


template<typename T>
void myPrint(T a, T b, T c)

	cout << "调用重载的模板" << endl;


void test01()

	//1、如果函数模板和普通函数都可以实现,优先调用普通函数
	// 注意 如果告诉编译器  普通函数是有的,但只是声明没有实现,或者不在当前文件内实现,就会报错找不到
	int a = 10;
	int b = 20;
	myPrint(a, b); //调用普通函数

	//2、可以通过空模板参数列表来强制调用函数模板
	myPrint<>(a, b); //调用函数模板

	//3、函数模板也可以发生重载
	int c = 30;
	myPrint(a, b, c); //调用重载的函数模板

	//4、 如果函数模板可以产生更好的匹配,优先调用函数模板
	char c1 = 'a';
	char c2 = 'b';
	myPrint(c1, c2); //调用函数模板


int main() 

	test01();

	system("pause");

	return 0;

总结:既然提供了函数模板,最好就不要提供普通函数,否则容易出现二义性

1.2.6 模板的局限性

局限性:

  • 模板的通用性并不是万能的

例如 :

	template<class T>
	void f(T a, T b)
	 
    	a = b;
    

在上述代码中提供的赋值操作,如果传入的a和b是一个数组,就无法实现了

再例如 :

	template<class T>
	void f(T a, T b)
	 
    	if(a > b)  ... 
    

在上述代码中,如果T的数据类型传入的是像Person这样的自定义数据类型,也无法正常运行

因此C++为了解决这种问题,提供模板的重载,可以为这些特定的类型提供具体化的模板

#include<iostream>
using namespace std;

#include <string>

class Person

public:
	Person(string name, int age)
	
		this->m_Name = name;
		this->m_Age = age;
	
	string m_Name;
	int m_Age;
;

//普通函数模板
template<class T>
bool myCompare(T& a, T& b)

	if (a == b)
	
		return true;
	
	else
	
		return false;
	



//具体化,显示具体化的原型和定意思以template<>开头,并通过名称来指出类型
//具体化优先于常规模板
template<> bool myCompare(Person& p1, Person& p2)

	if (p1.m_Name == p2.m_Name && p1.m_Age == p2.m_Age)
	
		return true;
	
	else
	
		return false;
	


void test01()

	int a = 10;
	int b = 20;
	//内置数据类型可以直接使用通用的函数模板
	bool ret = myCompare(a, b);
	if (ret)
	
		cout << "a == b " << endl;
	
	else
	
		cout << "a != b " << endl;
	


void test02()

	Person p1("Tom", 10);
	Person p2("Tom", 10);
	//自定义数据类型,不会调用普通的函数模板
	//可以创建具体化的Person数据类型的模板,用于特殊处理这个类型
	bool ret = myCompare(p1, p2);
	if (ret)
	
		cout << "p1 == p2 " << endl;
	
	else
	
		cout << "p1 != p2 " << endl;
	


int main() 

	test01();

	test02();

	system("pause");

	return 0;

总结 :

  • 利用具体化的模板,可以解决自定义类型的通用化
  • 学习模板并不是为了写模板,而是在STL能够运用系统提供的模板

1.3 类模板

1.3.1 类模板语法

类模板作用:

  • 建立一个通用类,类中的成员 数据类型可以不具体制定,用一个虚拟的类型来代表。

类模板语法:

template<typename T>

示例:

#include<iostream>
using namespace std;

#include <string>
//类模板
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;
	
public:
	NameType mName;
	AgeType mAge;
;

void test01()

	// 指定NameType 为string类型,AgeType 为 int类型
	Person<string, int>P1("孙悟空", 999);
	P1.showPerson();


int main() 

	test01();

	system("pause");

	return 0;

总结:类模板和函数模板语法相似,在声明模板template后面加类,此类称为类模板

1.3.2 类模板与函数模板区别

类模板与函数模板区别主要有两点 :

  1. 类模板没有自动类型推导的使用方式
  2. 类模板在模板参数列表中可以有默认参数

#include<iostream>
using namespace std;

#include <string>
//类模板
template<class NameType, class AgeType = int>
class Person

public:
	Person(NameType name, AgeType age)
	
		this->mName = name;
		this->mAge = age;
	
	void showPerson()
	
		cout << "name: " << this->mName << " age: " << this->mAge << endl;
	
public:
	NameType mName;
	AgeType mAge;
;

//1、类模板没有自动类型推导的使用方式
void test01()

	// Person p("孙悟空", 1000); // 错误 类模板使用时候,不可以用自动类型推导
	Person <string, int>p("孙悟空", 1000); //必须使用显示指定类型的方式,使用类模板
	p.showPerson();


//2、类模板在模板参数列表中可以有默认参数
void test02()

	Person <string> p("猪八戒", 999); //类模板中的模板参数列表 可以指定默认参数
	p.showPerson();


int main() 

	test01();

	test02();

	system("pause");

	return 0;

总结:

  • 类模板使用只能用显示指定类型方式
  • 类模板中的模板参数列表可以有默认参数

1.3.3 类模板中成员函数创建时机

类模板中成员函数和普通类中成员函数创建时机是有区别的:

  • 普通类中的成员函数一开始就可以创建
  • 类模板中的成员函数在调用时才创建
#include<iostream>
using namespace std;

class Person1

public:
	void showPer

《Exceptional c++》和《提高c++性能的编程技术》学习笔记

1、c++临时对象

创建对象是一个费时,费空间的操作,会产生临时对象的几种情况:

1)以值的方式给函数传参
按值传递时,首先将需要传给函数的参数,调用拷贝构造函数创建一个副本,所有在函数里的操作都是针对这个副本的,也正是因为这个原因,在函数体里对该副本进行任何操作,都不会影响原参数。

指导原则:在传递函数参数时,选择以常量引用的方式,而不是传值方式

2)类型转换
我们在做类型转换时,转换后的对象通常是一个临时对象。

构造函数应该避免隐式类型转换,会隐含产生临时变量。用explicit constructor代替隐式转换

3)函数需要返回一个对象时
当函数需要返回一个对象,他会在栈中创建一个临时对象,存储函数的返回值。

4)对于大部分容器(包括链表)而言,调用容器的end()函数将返回一个临时对象,并且这个对象需要被构造和析构。由于这个临时对象的值在循环中是不会改变的,因此如果在每次循环迭代中都重新计算,将会导致不必要的开销,实际上这个临时对象只需计算一次,将其保存到局部对象当中即可。

5)使用后置++时会产生临时对象,应多使用前置++。

2、优先采用”a op= b;”而不是”a = a op b;”进行运算

例如,为什么operator += 的效率更高?原因是这个运算符是直接对其左边的对象进行运算,并且返回的是一个引用,而不是临时对象。与此不同的是,operator+必须返回一个临时对象。

通常,如果定义了某个运算符op,还应该定义其相应的赋值运算符op=,并且用后者来实现前者。

3、重载、覆盖和隐藏

1)对函数f()进行重载(overload)是指,在相同作用域中定义另一个相同名字的函数,并且与f()有着不同的参数类型、顺序或数目。
2)对虚函数f()进行覆盖(override)是指,在派生类中定义一个相同名字的函数,并且这个函数的参数列表与f()相同。
3)对外层作用域(基类、外部类或名字空间)中的函数f()进行隐藏(hide)是指,在内层作用域中(派生类、嵌套类或嵌套名字空间)定义另一个相同名字的函数,这将隐藏定义在外层作用域中的同名函数。

4、缓式构造

在C++中,不自觉地在程序开始出预先定义所有对象的做法是一种浪费。因为这样可能会创建一些直到最后都没有用到的对象。好的编码风格应该是在用到这个变量的时候再创建。
例如:

if(...)

XXXObject obj;
XXXFuc(obj);

在条件语句之后再创建变量可以减少开支。如果在之前就创建了变量,结果又没有用到,就会有额外的开支。

5、虚函数(模板对继承的优势)

内联是在编译时决定的,编译器不可能把运行时才解析的虚函数设置为内联。这往往会导致性能问题,解决方法:
因为函数调用的动态绑定是继承的结果,所以消除动态绑定的一种方法是基于模板的设计来替代继承。模板把解析的步骤从运行期提前到编译期,并且可以在编译时使用内联,提高了性能,而编译时间的适当增加是可以接受的。基于模板的设计有两个优点:重用和效率。

6、内存池

内存池(Memory Pool)是一种内存分配方式。 通常我们习惯直接使用new、malloc等API申请分配内存,这样做的缺点在于:由于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能。

内存池则是在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的一个显著优点是尽量避免了内存碎片,使得内存分配效率得到提升。

7、内联

内联是用方法的代码来替换对方法的调用。内联通过消除调用开销来提升性能,并且允许进行调用间优化。内联的主要作用是对运行时间进行优化,当然他也可以使可执行映像变得更小。总结如下:

内联提升性能的两个方面:
1)调用间优化
调用间优化是面向某一方法的调用过程,基于对上下文场景更加全面的理解,使得编译器在源代码层面及机器代码层面对方法进行优化。这种优化的一般形式为:在编译期间进行一部分预处理,避免在运行时重复类似过程。
2)避免开销大的方法调用

内联缺点:1)代码膨胀。2)有些方法本身应避免内联,如递归。如果将递归函数A内联,编译器将不断循环尝试将A方法插入到A方法中,形成死循环。

8、引用计数

引用计数是内存管理的一个技巧,可以看做是一种简单的垃圾回收机制,它允许多个拥有共同值的对象共享同一个对象,省却了拷贝赋值的过程,更加节省内存和宝贵的CPU时间

引用计数的基本思想是将销毁对象的职责从客户端代码转移到对象本身。对象跟踪自身当前被引用的数目,而不具体记录是谁引用了它。在引用计数达到零时自行销毁 !!!降低了建立和解除引用的代价。。也就是,当对象不再被使用时自行销毁。引用计数提供了一种简洁高效的内存管理方法。

它简化了跟踪处理堆中对象的过程。一个对象被从堆中分配出来之后,我们需要明确的知道是谁拥有了这个对象,因为只有拥有这个对象的所有者能够销毁它。但我们在实际使用过程中, 这个对象可能被传递给另一个对象(例如通过传递指针参数),一旦这个过程复杂,我们很难确定谁最后拥有了这个对象。使用引用计数就可以抛开这个问题,我们不需要再去关心谁拥有了这个对象,因为我们把管理权交割给了对象自己当这个对象不再被任何人使用时,它自己负责销毁自己

总之,带有引用计数功能的智能指针兼有普通指针共享实值对象和auto_ptr自动释放实值对象的双重功能,并自动管理实值对象的生命周期和有效引用的计数,不会造成丢失引用、内存泄露和多次释放等问题。

但是,在引用计数的过程中,我们也丢失了重要的信息:到底是谁引用了自己。所以,引用计数在处理间接引用的问题上代价增加

9、上下文切换

上下文切换,有时也称做进程切换或任务切换,是指CPU 从一个进程或线程切换到另一个进程或线程。有较大时间开销。

这个过程要保存进程和处理器的状态。保存进程的状态是为了维护进程执行点的精确记录,保存处理器的状态是为了在相关进程继续执行时,处理器能返回原状态。每次进程换出时,与进程相关的处理器状态要从处理器移到内存里,每次进城换入时,处理器要把这部分数据加载进来。

上下文切换有三个主要开销:
1)处理器上下文迁移
2)缓存和TLB(旁路转换缓冲)丢失。每次上下文切换后,进程要重建它的缓存内容。
3)调度开销。调度器要决定是否继续执行中断的过程,或者是否把另外一个进程装载到处理器上。

10、

以上是关于C++学习8-C++提高编程的主要内容,如果未能解决你的问题,请参考以下文章

C++落选,2021年最想学习的五大编程语言

大牛C++编程开发学习建议50条

50条大牛C++编程开发学习建议

学习C++有啥用途?

C++实例学习视频教程

推荐几本学习c++的靠谱书