C++笔记

这篇笔记详细介绍了C++的面向对象编程(OOP)和泛型编程(GP)概念,涵盖了C++的新特性、类与对象、函数模板、运算符重载、继承与多态等方面,旨在帮助读者深入理解C++编程。
摘要由CSDN通过智能技术生成

这是我C++的学习笔记

在C的基础上,首先对面向过程编程(Procedure Oriented Programming)中的新特性作一些补充。本笔记主要着重于面向对象编程(Object Oriented Programming)(侧重于数据)和泛型编程(Generic Programming)(侧重于工具)。

一、C++新特新补充

  1. 头文件声明的变化
    头文件声明的变化以及对.h头文件的继承
    头文件的使用会影响对名称空间的使用。
  2. 名称空间namespace以及编译指令using
    名称空间相当于对名称(变量名称,函数名称,操作名称,etc.)再作一次封装,以应对不同厂商的不同函数/操作/变量撞名时的情况。可通过using指令全空间调用,也可通过“::”作用域运算符单独调用。
    对于未命名的名称空间,作用域为声明点到该区末尾。
    C++标准语言库为std。
using namespace space_name;
//全空间调用名称空间space_name,使能该空间定义的全部名称
//可放在函数体内部或外部,使在本函数或本文本中所有函数可用

using space_name::space_mem_name;
//仅使能space_name中的space_mem_name

...space_name::space_mem_name...
//抑或在调用时再通过::声明(调用多时会比较麻烦)
  1. cin和cout的使用
    cout的使用
    cout的使用
    \n和endl在cout中的使用。
    在cout中,亦可对输出流进行格式化。
    cout可通过多个<<进行拼接。
    cin的使用
type_name stream_target;
cin >> stream_target;
//将键盘输入流输入到stream_target对象中
//输入流将被自动转换为type_name类型

在这里插入图片描述

  1. bool类型
    布尔变量只有ture(非零值)和false(零值)两种值,赋值后变量会被自动转换。
bool para_1 = 100;	//para_1 assigned true
bool para_2 = -100;	//para_2 assigned true
bool para_3 = 0;	//para_3 assigned false
  1. 关于强制类型转换
    在这里插入图片描述
    强制类型转换不会改变value本身,而是会创建一个新的指定类型的值。
  2. 关于Cpp中的auto
    auto用于根据变量的输入值自动判断变量类型。
auto para = 100.1;	//para belongs to float type
  1. 关于输入函数的使用very key
cin;
cin.get();
cin.getline();
//以及以上的拼接,应对空格与换行符
//应对不同的赋参情况,系统将根据具体参数情况调用对应的重载函数版本

cin.eof() == 1;	//EOF被激活,否则返回0
cin.fail() == 1;	//EOF被激活,否则返回0
//事前检测
//使用EOF将会更智能地检测输入结束。对win下为CTRL+Z加ENTER,其他系统有所不同
cin.clear();
//使用cin.clear()清除EOF的**标记**从而恢复IO

在这里插入图片描述

  1. 使用string类的安全便捷,包括赋值拼接和添加操作。
  2. 比较数组,vector对象和array对象
vector<type_name> para_name(length);
arrar<type_name, length> para_name;

使用.at()限制array或vector检索区域,提高安全性。

  1. 对for循环,新增了基于范围的循环,对于数组或容器的所有元素执行相同的操作。
ary_type ary_name[num] = {...};
for (para_type para_name : ary_name)
	execute_content;
  1. 对于C++中文件读写,使用fstream头文件。并且像在控制台中使用cout一样将流写入文件中或读取文件。使用.open()函数。
  2. 关于引用变量
    在这里插入图片描述
    在这里插入图片描述
    必须在声明引用变量前将变量初始化(不同于指针)。引用在传参和指针传参的作用有点类似,并且更接近于const指针。
    引用多用于传递函数参数,这是对C中只能传递指针的一种超越。
int main()
{
	...;
	para_type & para_name;
	fun_name(para_name);
	...;
}
fun_type fun_name(para_type &para_name)
{
	...;
}
//如果不希望对para进行修改,则一你改改将para变为const变量,若出现修改则报错。key:const与引用变量结合应用
//在main中调用fun_name()时参数应直接用变量而不能用表达式。
//应注意函数不得返回指向临时变量的引用
//需要引用返回值,需要将函数类型变为const引用类型(非const返回值储存于临时单元,使用后会被释放)

在这里插入图片描述

  1. 函数模板(此外还有类模板,后面再谈)
    函数模板用于在应对不同类型参数实现相同功能的情况下,通过编译器自动调整函数体内类型从而简化程序的一种手段。
    Key:代码中模板本身不会生成函数定义,而是指导生成一个函数定义的方案。
template <typename TN>
//typename可替换成class(推荐使用前者),TN=TypeName,用户自定义
func_type func_name(TN para_0, ...)
{
	...
	TN para_1;	//新建一个临时参数
	para_0;		//使用传入的para_0
}
//传入参数时编译器会自动检查参数类型并作自动替换
  1. 重载模板
    在同一个模板名下定义算法不同的模板,编译器会根据调用时不同的参数输入自动匹配不同的算法。
template <typename TN>
func_type func_name(para_lib_1)
{alg_1;}

template <typename TN>
func_type func_name(para_lib_2)
{alg_2;}

//编译器根据调用时参数形式是para_lib_1还是para_lib_2,进而调用不同版本的算法
//这种手法对于含数组,结构体之类的参数很有用
  1. 具体化和实例化
    隐式实例化
    直接从函数模板生成的实例,统称为(隐式)实例化。
    显式具体化
    对于某些特殊类型,可能不适合模板实现,需要重新定义实现,显式具体化基于函数模板的,函数模板的基础上,添加一个专门针对特定类型的、实现方式不同的具体化函数。
template <> func_type func_name<para_type>(para_lib)
{...;}
//显式具体化,当遇到para_type类型参数时,编译器优先调用该型,而不是经过默认模板

显式实例化
当显式实例化模板时,在使用模板之前,编译器根据显式实例化指定的类型生成模板实例。显式实例化只需声明,不需要重新定义。

template func_type func_name<para_type>(para_lib)
{...;}
//显式实例,当遇到para_type类型参数时,编译器根据原模板的定义及该声明直接生成一个实例函数,该函数仅接受para_type型

在这里插入图片描述

  1. 函数版本的选择
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

  2. decltype的使用

decltype(x) y;	//使y的类型与x相同,二者可以为表达式
//整个遍历的过程将会十分复杂,因为x,y名可以为常规变量,函数或者结构等。

在更新之后,使用auto声明可以为函数服务。

auto func_name(para_lib)	//decltype在此处无能为力,看不到返回值的类型
{
	...;
	return para_1 + para_2;	//返回结果类型未知
}
  1. 处理预防重复声明的情况
#ifndef content	
...
#endif
  1. 外部链接性(代码块外,或在但文件中使用extern),内部链接性(代码块外,static限定),无链接性(代码块内)
    在这里插入图片描述
    此外还有thread_local,mutable,volatile等一类
    Key:const对外部链接性的影响。
    此外,函数也有extern(可选)和static之分。
  2. 作用域运算符“::”
  3. 动态储存
type_name *start_poi_name = new type_name (para);
//para为初始值
type_name *start_poi_name = new type_name {ary_lib};
//对数组,结构体亦类似
type_name *start_poi_name = new (location_name) obj_name;
//在location_name中建立空间分配给obj_name
//失败时,返回空指针
delete(poi_name);
//释放空间

二、面向对象编程(OOP)

面向对象编程,是一种编程风格。将数据与处理过程剥离,使数据私有不可见,通过类的公共接口与处理过程关联,实现对数据的调用。数据不能直接访问,只能通过接口函数调用

:将数据表示和操纵数据的方法封装成一个整体包。(类可以看作一种数据类型)
_一般将类的首字母大写。
_类声明(包括数据和接口)放置在一个.h头文件,方法的定义放置在一个.cpp文件中。
对象:相当于变量,包含所属类的数据和方法。
成员:包括类中声明的各个数据和各个接口函数。

1.类和对象的声明,方法的定义
//Declare a class
//This is a .h file
//head files & parameters declare

#ifndef .h_file
#define .h_file
class class_name
{
private:
//This line could be neglected
	para_lib;
public:
	method_lib;
};
#endif
//Declare methods
//a .cpp file
//head files & parameters declare
#include .h_file

func_type class_name::method_name(para_lib)
//Using :: operator
{operations;}
......
//method members could be varited
//Use class to declare an object
//a .cpp file
//head files & parameters declare

class_name obj_name;
int main()
{
	...
	obj_name.method_name(para_lib);
	...
	//also could do in this way, no need for a object name but use a point to conduct
	class_name * poi_name = new class_name(initialize_lib);
	...
}
2.对象的初始化

由于数据私有,不能直接对对象的数据进行初始化。只能通过类构造函数进行初始化(一般该函数与类名相同),对象使用完成后,通过构析函数完成内存清理工作(一般该过程由编译器自动完成,对于堆栈和静态储存的性质过程会有所不同)(构析函数名为类名前加~)。

//Declare and define an initializing function
//placed in public zone

class_name::class_name(initialize_lib)
//No need for type name, and it return nothing
//initialize_lib should not include private members' name
{
	initialize operations;
	...
}

//In a .cpp file, when need to initialize an object
{
	...
	class_name obj_name(initialize_lib);
	//"class_name obj_name = class_name(initialize_lib)" in short
	//or use point and "new" operation
	...
}

编译器会自动提供默认构造函数:

class_name::class_name() {}
//do noting

一旦给出自定义构造函数,程序员就必须自行提供默认构造函数(可用函数重载实现多种对象初始化函数)。其中,隐式调用默认构造函数不需要加括号(否则为返回该类的函数)。
构析函数:

class_name::~class_name() {}

对于const数据成员和引用数据成员,其需要在执行构造函数之前(即创立对象之前)就对其进行初始化,通过成员初始化列表执行初始化工作(实际上,不仅针对常量成员,所有数据成员据可以用上述方式初始化,多个时在后面补上,用逗号","隔开),注意,这种语法仅能用于构造函数。如下例子:

//.h file
class class_name
{
private:
	const para_type para;
	...
public:
	class_name(para_lib){...};
	...
}

//.cpp file
...
para_type ini_para;
class_name::class_name(para_lib) : para(ini_para)	//initialize constant para
{...}
...
3.const成员函数

当声明对象为const类型时,调用接口时并不能保证是否会对对象做出修改(无论是否真的会改),此时编译器一定会报错。为解决这个问题,对接口做出修改:

//.h flie
...
public:
	func_type func_name const(para_lib);
...

//.cpp file
func_type class_name::func_name const(para_lib) {...}

//.cpp file
...
const class_name obj_name(initialize_lib);
obj_name.func_name(para_lib);
4.this指针和对象数组

在调用两个对象时,我们一般有两种方法。一种是通过提供返回对象成员的函数接口,在类的域外进行数据处理(一般最好加const限定成员不会被修改)。比如:

//.h file
class class_name
{
private:
	para_type para;
public:
	fun_type fun_name(para_lib) const;
	//in case of being rewrited
}

//.cpp file
#include ".h"
class_name::fun_name(para_lib) const
{
	using namespace std;
	return para;
}

//.cpp file
#include ".h"
using namespace std;
int  main()
{
	...
	class_name obj_1, obj_2;
	para_type para_1, para_2;
	para_1 = obj_1.fun_name(para_lib);
	para_2 = obj_2.fun_name(para_lib);
	...
	return 0;
}

现在介绍一种使用this指针的更加简洁的方法,将一个对象传入另一个对象的成员接口函数中,直接在类的作用域中完成操作。

//.h file
class class_name
{
private:
	para_type para;
public:
	const fun_type & fun_name(const class_name & obj) const;
	//return a const type object. transfer a const type object into another object. in case of rewriting any data as well
}

//.cpp file
#include ".h"
class_name::fun_name(const class_name & obj) const
{
	using namespace std;
	...
	return obj;
	//the transfered one
	...
	return *this
	//current object itself
	//"this" is just a pointer while "*this" is the content that we need (the whole object)
	//further more, when need the member of the result, use "->" operator like "this->para"
}

以上用两个对象的案例阐述了this指针的作用,对于有两个以上对象时,使用对象数组:

//.cpp file
int main()
{
	...
	class_name obj_ary_name[para] = 
	{
		...initializing_lib...
		//use "," to separate
	}
}

初始化时每个元素的初始化与单个对象初始化规则相同,没有自定义就默认,可能要重载。
对于数组,通常通过循环指令两两之间逐个进行操作。

5.类的作用域

类中定义的数据与函数作用域为整个类,在类外不可知。针对类、对象和指针,依次分别采用“::”、“.”、“->”三种运算符进一步定位。
声明类时,不能直接用const限定,因为类的声明不占用空间,只用创建对象时才建立存储空间。如:

private:
	...
	const para_type para_name = para;
	//forbidden
	...

解决方法,一是使用枚举变量,二是改用声明static变量将其存储在静态存储区而不是对象中。

//use emun variate (single)
private:
	...
	enum {para_name = para};
	para_type para_name;
	...

//use emun variate (multiple)
private:
	...
	enum class para_name_1 = {para_lib_1};
	enum class para_name_2 = {para_lib_2};
	para_type para_name_1;
	para_type para_name_2;
	...
//.cpp file
	...
	para_name_1::para_mem_1;
	para_name_2::para_mem_2;
	//multiple enum variate could be palced in the same class

//use static variate
private:
	...
	static const para_type para_name = para;
	para_type para_name;
	...
6.运算符重载

在函数重载中,应对不同的数据类型,我们能用同名的函数完成逻辑相同但形式不同的操作。运算符重载的道理亦相同,实际上运算符本身对标准类型就会有重载,对于类运算,我们需要使用operator运算自定义重载形式。

//.h file
class class_name
{
public:
	func_type operator+(para_lib);
	//重载+运算符,声明
	...
}

//.cpp file
func_type operator+(para_lib)
{...}
//定义新运算符样式

//.cpp file
class_name obj1, obj2, obj3;
obj1 = obj2 + obj3;
obj1 = obj2 operator+(obj3);
//以上两种等价,注意需要区分obj2和obj3的次序

运算符重载限制:不能为标准类型重载运算符,不能违反原本运算逻辑,不能修改优先级,不能创建新的运算符,部分运算符不能被重载。

7.友元

友元包括友元函数,友元类,友元成员函数。
友元在类中带friend声明,与成员权限相同,但不是成员,且不带friend和不用限定符定义。
友元应用的一个例子,就是和运算符重载结合在一起,解决交换律的次序问题。

//.h file
class c_n
{
	...
public:
	...
	func_type func_name1(para_lib);
	friend func_type func_name2(para_lib);
	//声明成元函数与友元函数
	...
}

//.cpp file
...
func_type c_n::func_name1(para_lib)
{...}
func_type func_name2(para_lib)
{...}

注意:成员函数和友元函数的传参情况有所不同

8.类与基本类型的转换

类与基本类型的转换,有从基本类型到类,和从类到基本类型两种。
从基本类型到类,需要有类声明中有接受唯一参数的构造函数作为转换函数(要求不应有二义性,即对一种类型仅适配一个单参构造函数),编译器将自动将数据匹配到私有成员中。
注意,从基本类型到类的转换包括隐性转换和显性转换两种。应尽量避免使用隐性转换。

//.h file
class c_n
{
...
public:
	...
	c_n(para_type para);
	//隐性/显性转换声明
	explicit c_n(para_type para);
	//此时仅支持显性转换
}

//.cpp file
...
para_type para;
c_n obj;
obj = para;
//隐性转换
obj = c_n(para);
//显性转换

从类转换到普通类型,需要提供没有类型和没有参数的转换函数(成员函数),该函数返回目标类型

//.h file
class c_n
{
private:
	...
	para_type para;
	...
public:
	...
	fuc_name();
	...
}

//.cpp file
...
c_n::func_name()
{return para;}

//.cpp file
...
para_type para_1;
c_n obj;
para_1 = obj; 
...
9.使用类过程中的内存管理

在使用类的过程中,应该理清在赋值,传参和返回三个过程中,类在内存中的具体存储情况。
主要注意以下几个方面:

  • 针对的过程:类之间的赋值,传递类参数,返回类。(其实上述三种本质上相同)
  • 调用类的方式:传值,传递指针,传递引用,以及以上三者是否组合const
  • 调用是否会触发类的一些自动执行步骤:构造函数,析构函数,以及static成员

值得注意的是,在传值过程中一般会误触发自动执行步骤导致static成员被更改;而传指针会造成重复释放内存,造成程序数据出错(需要进行深度复制)。要思考new和delete在每个步骤的触发情况,以及形式的对应情况。
Key:静态成员函数不能通过对象来调用(无关联性),且只能采用静态成员数据。
Key:指针多数与new和delete连用。
Key:函数返回可以返回(const)对象,返回(const)对象的引用,是否引用的区别在于是否会触发类的自动进程(构造和析构),返回类型应与声明是否含const匹配。引用的效率会更高。

10. 类的继承(is-a关系下的公有继承)

继承可以实现类的扩展和修改,包括再已有类上添加功能、成员和修改类方法,并且不需要访问源代码。
继承是一个类派生出另一个类,原始类为基类,继承类为派生类。以下是一个公有派生:

class c_1
{...};
//base class
class c_2 : public c_1
{...};
//derive class

派生类声明一般与基类声明放在同一个.h文件中。
在权限上,派生类继承了基类的成员和接口,对于基类的私有成员,派生类只能通过基类的接口访问。在扩展上,派生类可以添加自己的数据成员、成员函数和构造函数(服务于新成员)。但是,在基类中添加protected类可使派生类直接访问当中的数据成员。

class c_base
{
private:
	...
protected:
	...	//could be accessed derectly by ex class
private:
	...
}

派生类的初始化,需要通过在给派生类分配内存空间前先通过基类构造函数初始化基类对象,再初始化派生类对象(进入派生类的构造函数中)。而释放对象顺序与上述相反。

//.h file
class cder : public cbase
{...}

//.cpp file
cder::cder(para_lib_1) : cbase(para_lib_2)
{...};

派生类和基类的关系(这个很有趣)

  • 派生类对象可以使用基类的非私有的方法(这个已经知道了)
  • 基类指针或引用可以再不进行希纳是类型转换的情况下指向派生类对象,但是,这种指针或引用只能调用基类的方法(不能调用派生类方法)。(如以下例子)
//.h file
class cbase
{
...
public: 
	func_type men1();
	...
};
class cder : public cbase
{
...
public: 
	func_type men2();
	...
};

//.cpp file
cder obj;
cbase * p = obj;
cbase & r = obj;

p->men1();
r.men1();

p->men2();	//invalid
r.men2();	//invalid
  • 可以将派生类对象赋给基类对象,包括赋值、传参、引用等方式。因为派生类含的信息包含了基类,相当于向下兼容。例如:
//包含了上面代码块的.h file
//.cpp file

#include <".h file">
cder obj;
cbase obj_2 = obj;

虚函数实现公有多态继承。即对于同一个方法,会根据所受调用的对象不同而执行不同的行为。通过关键字virtual实现。

  • virtual声明的函数,程序将根据对象的类型,或者是引用或指针指向的对象调用相应的方法。如果没有virtual声明,程序将根据引用或指针的类型选择方法。(见例子)
  • 注意区别多态继承和函数重载。对于重载函数,必须在基类和派生类的对象中对所有的重载版本都声明virtual。如果只有部分声明,则剩余的在派生类中剩下没有声明virtual的重载版本将会被屏蔽,只能通过基类来调用(如果剩下的没有被修改那倒无妨)。
  • 虚函数主要保证函数同名即可,允许返回类型变化(返回类型协变)。
  • 在虚函数的定义中如果使用到虚函数,必须标明作用域(否则可能会引起混乱或陷入死循环(常见))
  • 若定义了虚函数,则通常要搭配使用虚析构函数(不同方法的内存释放方式不同)。而构造函数不能是虚函数(这样做没有意义,想想就清楚)。
  • 只有成员函数才能声明virtual,友元函数不是成员函数故不能被声明。
//.h file
class cbase
{
...
public: 
	func_type men0()
	{operation_1};
	virtual func_type men(paralib1)
	{
		operation_2;
		...
		cder::men(paralib1);
		//use "::" operator
		...
	}
	virtual func_type men(paralib2)
	{operation_3};
	//func_type could be varied
	...
};
class cder : public cbase
{
...
public: 
	func_type men0()
	{operation_4};
	virtual func_type men(paralib1)
	{operation_5};
	virtual func_type men(paralib2)
	{operation_6};
	//func_type could be varied
	...
};

//.cpp file
...
cbase obj1;
cder obj2;
cbase & r1 = obj1;
cbase & r2 = obj2;	//assumpt it is valided

obj1.men(paralib1);	//use operation2
obj2.men(paralib1);	//use operation5

r1.men0(paralib1);	//use operation1
r2.men0(paralib1);	//use operation4

r1.men(paralib1);	//use operation2
r2.men(paralib1);	//use operation5

虚函数的出现使编译器开发出动态联编的功能(联编:将源码中的函数调用连接到指定的函数块代码)。虚函数通过形成隐藏的虚函数表数组,在执行程序时再链接函数块,以效率降低和存储体积变大为代价。
在虚函数再进一步,就是纯虚函数。声明包含纯虚函数的类不能创建对象。

virtual func_type func_name(para_lib) = 0;	//a pure virtual function

由此,则可以引出抽象基类(ABC,其不能创建对象),抽象基类至少包含一个纯虚函数,由抽象基类可以派生出很多有共同特征的派生类,他们之间特征的交集(共同特征)就是抽象基类。进而通过在派生类中重定义纯虚函数为虚函数,完成具体特征的描述。
最后,关于动态内存分配。若派生类使用了new,则必须为派生类定义显式析构函数、复制构造函数和赋值运算符。(再展开一下吧)

三、泛型编程(GP)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值