初步窥探 C++

文章目录

😍C++ 语言

传统C++是对C的升级扩充(1.对C语言的完善 2.支持面向对象编程)。 //效率 安全

由于C++兼容C的大部分特性(现代C++已经不能完成兼容C程序了),C++并不是一门完全面向对象的语言。(既有面向对象、又有面向过程、支持泛型编程)。

😊一、前言

windows 下的 VScode 通过 ssh 连接虚拟机。

setp虚拟机命令备注
1sudo apt-get install openssh-serve安装openssh
2sudo service ssh start启动ssh
3/etc/init.d/ssh restart在更改配置后重启ssh
1/etc/ssh/sshd_configssh的默认配置文件路径
2Port 22
PermitRootLogin yes
PubkeyAuthentication yes
配置文件的主要内容
setpwindows命令备注
1where ssh查看ssh服务位置
2Remote DevelopmentVScode安装插件
3Remote.SSH: Config File在设置里配置配置文件路径及文件名
4Remote.SSH: Path配置ssh服务路径及可执行文件名
5C:\Users\ChengYunhao.ssh\config配置\.ssh\config文件的用户名和IP
^\s*(?=\r?$)\n	// 正则解释:行开头 任意个空白符 以回车符结尾 或以换行符结尾

1.1 C++关键字

ISO C++98/03共63个(黑色)

ISO C++11新增10个 (绿色)

asmdoifreturntypedef
autodoubleinlineshorttypeid
booldynamic_castintsignedtypename
breakelselongsizeofunion
caseenummutablestaticunsigned
catchexplicitnamespacestatic_castusing
charexportnewstructvirtual
classexternoperatorswitchvoid
constfalseprivatetemplatevolatile
const_castfloatprotectedthiswchar_t
continueforpublicthrowwhile
defaultfriendregistertrue
deletegotoreinterpret_casttry
alignasalignofchar16_tchar32_tconstexpr
decltypenoexceptnullptrstatic_assertthread_local

1.2 C++运算符1

类别运算符结合性
后缀() [] -> . ++ -- 从左到右
一元+ - ! ~ ++ -- (type) * & sizeof从右到左
乘除* / %从左到右
加减+ -从左到右
移位<< >>从左到右
关系< <= > >=从左到右
相等== !=从左到右
位与 AND&从左到右
位异或 XOR^从左到右
位或 OR|从左到右
逻辑与 AND&&从左到右
逻辑或 OR||从左到右
条件?:从右到左
赋值= += -= *= /= %= >>= <<= &= ^= |= 从右到左
逗号, 从左到右

1.3 调试宏

void fun(void)
{
 	std::cout<<__func__<<std::endl;		//打印函数名
    std::cout<<__LINE__<<std::endl;		//打印代码所在行号
}

1.4 避免重复加载宏

#pragma once是写在头文件开头的编译指令,使得编译器能够自动帮我们实现“只编译一次该头文件”,从而避免了多次include该头文件导致的重复定义/声明问题。

#pragma once
// 等效于下面代码
#ifndef __HEAD_H_
#define __HEAD_H__

//头文件中的定义、声明
//...

#endif
  1. 较早的编译器并不支持#pragma once

  2. 如果两个不一样的头文件用了同一个宏名,用#ifndef的方法就会出问题,相当于扔掉了一个头文件


1.5 extern "C"

  extern "c" 的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上 extern "C" 后,会指示编译器这部分的代码按C语言,而不是C++的方式进行编译。

  由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。

这个功能主要用在以下情况:

  1. C++代码调用C语言代码;
  2. 在C++的头文件中使用;
  3. 在多人协同开发时,有的人比较擅长C语言,而有的比较擅长C++,这样的情况下也会有用到。

  由于 C 和 C++ 编译器对函数的编译处理是不完全相同的,尤其对于 C++ 来说,支持函数的重载,编译后的函数一般是以函数名和形参类型来命名的。

  例如函数 void fun(int, int) 编译后的可能是 _fun_int_int----不同编译器可能不同,但都采用了相似机制,用函数名和参数类型来命名编译后的函数名;而C语言没有类似的重载机制,一般是利用函数名来指明编译后的函数名的,对应上面的函数可能会是 _fun 这样的名字。

  因此,如果不加 extern "c",在链接阶段,链接器会从 一个源程序 生成的目标文件 *.obj 中找 _fun_int_int 这样的函数符号,显然这是不可能找到的,因为 fun() 函数被编译成了_fun 的符号,因此会出现链接错误

#ifndef __INCvxWorksh /*防止该头文件被重复引用*/
#define __INCvxWorksh

#ifdef __cplusplus //告诉编译器,这部分代码按C语言的格式进行编译,而不是C++的
extern "C"
{
#endif

    /*…*/

#ifdef __cplusplus
}
#endif

#endif /*end of __INCvxWorksh*/

使用方法:

extern "C" double sqrt(double);	// 单一语句

extern "C"		// 复合语句
{
      double sqrt(double);
      int min(int, int);
}

😥二、C++和C的语法差异

2.1 新增IO

// using namespace std;

std::cout << "ni hao" << str << std::endl;
std::cin >> i;

2.2 更加严格的类型检查

更加严格的类型检查。

//报警告
char *p = "str";

ISO C++ forbids converting a string constant to ‘char*’ [-Wwrite-strings]

ISO C++禁止将字符串常量转换为char *


2.3 更严格的const

C++中无法用指针修改const修饰的变量。

const int i;	//C++中会报错,在C中并不会
// 在 C++ 不会报错,但在运行后会发现并未修改 const 修饰的变量(严格的说此时应该是 常量 了)
// 在 C 中可以修改
const int i = 10;
int *p = (int *)&i;
*p = 5;

2.4 函数重载

C++允许函数同名,但需要参数列表(类型、个数、顺序)不同。

// 函数重载
bool compare(int a,int b)
{
    return a > b;
}
bool  compare(double a,double b)
{
    return a > b;
}

// 报错: 无法重载仅按返回类型区分的函数
int fun()
{
	cout<<"return fun"<<endl;
}
void fun()
{
	cout<<"fun"<<endl;
}

C语言不存在函数重载。

C++根据函数名参数个数参数类型判断重载,属于静多态,必须同一作用域下才叫重载。


2.5 函数的参数的默认值

允许函数声明中带参数 ,表示该函数的默认值。

void fun(char a = 'a')
{
 cout<<"char fun "<<a<<endl;
}

但在会在重名的函数重载时冲突


//报错
void fun(char a = 'a')
{
	cout<<"char fun "<<a<<endl;
}
void fun()
{
	cout<<"fun"<<endl;
}

有多个 重载函数 “fun” 实例与参数列表匹配


//无错误无警告
int fun(int a, int b = 5)
{
	printf("%d,%d", a, b);
	return 0;
}
//报错
int fun2(char a = 'b', int)
{
	printf("%c,%d", a, b);
	return 0;
}

default argument missing for parameter 2 of int fun2(char, int)

int fun2(char, int) 的参数2缺少默认实参

给定默认参数需要 从右向左依次 给定。

// 这样定义也是没有错误的,因为它属于同一个函数
int  fun(int a ,int b = 10);
int  fun(int a = 20,int b);
// 并且等价于
int fun(int a = 20,int b = 10);

2.6 引用 和 指针

  1. 指针是一个实体,而引用仅是个别名;

  2. 引用使用时无需解引用 *,指针需要解引用;

  3. 引用只能在定义时被初始化一次,之后不可变;指针可变; 引用“从一而终” _

  4. 引用没有 const,指针有 const,const 的指针不可变;

  5. 引用不能为空,指针可以为空;

    引用比指针更安全。由于不存在空引用,并且引用一旦被初始化为指向一个对象,它就不能被改变为另一个对象的引用,因此引用很安全。

  6. sizeof 引用得到的是所指向的变量(对象)的大小,而sizeof 指针得到的是指针本身(所指向的变量或对象的地址)的大小

  7. 指针和引用的自增(++)运算意义不一样;

    例如就++操作而言,对引用的操作直接反应到所指向的对象,而不是改变指向;而对指针的操作,会使指针指向下一个对象,而不是改变所指对象的内容。


2.7 内联函数 和 宏函数

宏函数

  函数式宏是在编译时展开并填入程序的,宏函数只是做宏展开,不做参数传递、函数调用、函数引用和返回值的传递。

因此:

  函数式宏能是程序的运行速度稍微提高一点儿,但是当函数中有大量的宏替换的时候,又会使得程序变得臃肿。(原理是宏函数只做替换,并没有类似函数调用的跳转、参数出入栈等操作,自然会提高函数的运行速度。这种运行速度加快体验在大量使用的时候尤为明显)。

宏在使用的时候必须小心谨慎,避免出现问题。这一点是有宏的本身特性决定,即只做替换不做计算。

情况一

#define sqr(a) ((a)*(a))

调用该函数式宏计算sqr(a++),展开后就变为:((a++) * (a++)),可以发现a执行了两次自增操作。


情况二

// 假如我们在sqr 和’('之间多敲了一个空格,如下
#define sqr (a) ((a)*(a))

那么此时函数式宏就变成了,宏定义了,也成对象式宏。即sqr会被编译器替换成(x) (x)*(x)


情况三

#define sum(x,y) x + y	//注意:不规范的函数式宏的定义

//调用
z = sum(a,b) * sum(c,d);
//编译器将其展开后就变为:
z = a + b * c + d;	//这样是不是偏离了我们的本意

因此,我们在定义函数式宏的时候与一定要每个参数以及整个表达式都用()括起来,就不会出错了。

上面的就可以改为

#define sum(x,y) ((x) + (y))	//正确的定义方法

//调用
z = sum(a,b) * sum(c,d);
/编译器将其展开后就变为:
z = ((a) + (b)) * ((c) + (d));

情况四

#define puts_alert(str)  {putchar('\a');puts(str);}

int main(int argc,char *argv[])
{
    .....
    if(n)
        puts_alert("这个数不是0.");
    else
        puts_alert("这个数是0.");
    .....
}

当我们运行这个程序的时候会报错,无法运行。提示else 缺少if

因为展开后是这样的

if(n)
	{putchar('\a');puts(str);};
else
	{putchar('\a');puts(str);};

很明显,if下的复合语句’}‘后的’;‘会被认为是空语句,那么此时else再去找它上面的那个if的时候就找不到了,因此就会报错。当然了函数式宏中的’{}'也不能少,若少了又会报其他的错误。

利用逗号表达式

#define puts_alert(str)  (putchar('\a'),puts(str))
//在if处展开
if(n)
	(putchar('\a'),puts(str));
else
	(putchar('\a'),puts(str));

宏函数在使用时很方便,正确使用的话不仅可以使得我们的程序变得简洁,而且可以提高我们程序的运行速度。

内联函数
  • 用关键字inline修饰的函数就是内联函数。关键字在函数声明和定义的时候都要加上,不写系统还是会当成常规函数。
  • 类内定义的函数都是内联函数,不管是否有inline修饰符。
  1. 内联含函数比一般函数在前面多一个 inline 修饰符。

  2. 内联函数是直接复制“镶嵌”到主调函数中去的,就是将内联函数的代码直接放在内联函数的位置上,而由于内联函数是将函数的代码直接放在了函数的位置上,所以没有指令跳转,指令按顺序执行。

  3. 内联函数是程序中调用几次内联函数,内联函数的代码就会复制几份放在对应的位置上。

  4. 内联函数一般在头文件中定义,而一般函数在头文件中声明,在cpp中定义。

内联函数的目的:修补宏的缺陷(不能调试的缺陷)

内联函数与宏函数

宏函数内联函数
类型字符替换真正的函数
何时展开预处理运行时

内联函数和宏很类似,区别在于:

宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。

1 内联函数是真正的函数:只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。你可以象调用函数一样来调用内联函数,而不必担心会产生像处理宏时的一些问题。

**2 内联函数也有一定的局限性。**就是函数中的执行代码不能太多了,如果,内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数。这样,内联函数就和普通函数执行效率一样了。

3 内联函数是不能为虚函数的,但样子上写成了内联的,即隐含的内联方式。在某种情况下,虽然有些函数我们声明为了所谓“内联”方式,但有时系统也会把它当作普通的函数来处理,这里的虚函数也一样,虽然同样被声明为了所谓“内联”方式,但系统会把它当然非内联的方式来处理。


2.8 作用域

作用范围类别语句
最大全局作用域extern
(C++新增)名字空间作用域namespace
(C++新增)类作用域
最小模块作用域函数和语句

***名字空间作用域:***就是程序员利用名字空间定义在C++程序中划分出来的一块比较大的程序区段。在该程序区段内部,可以定义类型,函数,模版,变量。名字空间作用域可以跨越多个*.cpp文件而存在。在名字空间作用域内部还可以继续定义其他的名字空间作用域,也就是说,名字空间作用域是可以互相嵌套的。


2.9 newdelete

malloc()free()是C语言中动态申请内存和释放内存的标准库中的函数。而newdeleteC++运算符、关键字。newdelete底层其实还是调用了mallocfree

stepnewdelete
1调用operator new分配空间调用析构函数清理对象
2调用构造函数初始化对象调用operator delete函数释放空间
  • mallocfree是函数,newdelete是运算符。
  • malloc在分配内存前需要大小,new不需要。
  • malloc不安全,需要手动类型转换,new不需要类型转换
  • free只释放空间,delete先调用析构函数再释放空间(如果需要)。
  • new是先调用构造函数再申请空间(如果需要)。
  • 内存不足(开辟失败)时处理方式不同。malloc失败返回0,new失败抛出bad_alloc异常。
  • newmalloc开辟内存的位置不同。malloc开辟在堆区,new开辟在自由存储区域。
  • new可以调用malloc(),但malloc不能调用new
class Test
{
    Test(int val);
    ...
}
Test *p = new Test(15);
int *q = new int [30];
delete p;
delete q;

2.10 运算符重载

运算符重载的本质为函数重载,是重载系统内部的运算符函数,是实现类 静态多态性 的方式之一。

不能创造新的运算符,只能重载C++中已有的运算符,并且规定有6个运算符不能重载

不能重载的运算符含义
.类属关系运算符
.*成员指针运算符
::作用域运算符
?:条件运算符
#编译预处理符号
sizeof取数据类型的长度

  运算符重载是针对新类型的实际需求,对原有的运算符进行适当的改造。一般来讲,重载后的运算符的功能应当与运算符的实际意义相符。

  我们平常常见的算术运算符、逻辑运算符等运算符都是系统库函数里面已经存在的,所以运算符可以直接用于我们常用的数据类型。然而对于我们自定义的类实例化的对象,系统没有与之相关的运算符可以操作,但是为了使我们编写程序时更加便捷,C++提供了一种方式——运算符重载,来满足我们对于类对象进行的操作。

运算符重载的形式:<返回类型说明符> operator<要重载的运算符> <参数列表>

如:Test& operaor+(Test &obj);

运算符重载的实现方式

  1. 类成员函数:publicprivateprotected任意访问权限都可。

  2. 友元函数:同上。

  3. 普通函数

  • <<>> 输入和输出流一般作为友元重载,不能作为成员函数重载

😏三、类和对象

面向对象特征:抽象,封装,继承,多态。

  • 对象:是一个实体,我们眼睛看到的所有实体都可以看成一个实体对象。
  • 类:是用来对实体(对象)进行描述的。(对象有什么属性,有什么功能)类是一种自定义类型。
  • 方法:是实现类功能的一个具体实现,该类有什么样的功能?类的所有功能都要通过调用方法来实现。

C++为了兼容c语言,在这里我们也可以使用struct来定义类。和c不同的是,C++中的struct可以在内部放置函数,除此以外两者定义出的类还有其他的的区别。

structclass
成员默认权限publicprivate
默认继承方式publicprivate
定义模板参数可以定义模板参数可以定义模板参数

3.1 定义类

class 类名//或者struct 类名
{
	//类体:成员变量--属性  成员函数---功能
};

  • 类名首字母大写

3.2 访问权限

控制哪些成员可以在类外直接被访问,哪些只能通过方法进行访问。

访问限定符的作用域从该访问限定符出现的位置,知道到下一个访问限定符出现的位置为止。

  • public 表示共有;

    类的数据成员和函数可以被该类对象和派生类访问。

  • private 私有型;

    自己的类可以访问,但派生类不能访问。

  • protected 保护型;

    自身类和派生类可以访问相当于自身的private型成员,它同private的区别就是在对待派生类的区别上

访问限定符publicprivateprotected
释义公有型私有型保护型
谁可访问1. 该类中的函数
2. 子类的函数
3. 其友元函数访问
4. 该类的对象访问
1. 该类中的函数
2. 其友元函数访问
1. 该类中的函数
2. 子类的函数
3. 其友元函数访问
不能访问任何其他均不能访问该类的对象访问
继承后的属性

友元函数包括3种:

  • 友元函数(设为友元的普通的非成员函数)
  • 友元类(设为友元的其他类的成员函数)
  • 友元成员函数(设为友元类中的所有成员函数)

3.3 构造和析构

  类的构造函数析构函数都是类的一种特殊的成员函数,在程序中未定义构造函数和析构函数时,编译器会自行定义一个。

  构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回void。构造函数可用于为某些成员变量设置初始值,它会在每次创建类的新对象时执行。

  默认的构造函数没有任何参数,但如果需要,构造函数也可以带有参数。这样在创建对象时就会给对象赋初始值。

  与其他函数不同,构造函数除了有名字,参数列表和函数体之外,还可以有初始化列表,初始化列表以冒号开头,后跟一系列以逗号分隔的初始化字段。

必须使用初始化列表的时候:
  1. 常量成员。

    因为常量只能初始化不能赋值,所以必须放在初始化列表里面

  2. 引用类型。

    引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面

  3. 没有默认构造函数的类类型。

    因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化

  4. 高效。

    使用初始化列表少了一次调用默认构造函数的过程,这对于数据密集型的类来说,是非常高效的

    所以一个好的原则是,能使用初始化列表的时候尽量使用初始化列表

class foo
{
public:
	foo(string s, int i):name(s), id(i){} ; // 初始化列表
private:
	string name ;int id ;
};
提醒:在函数的声明和定义分离时,初始化列表必须在定义处、而函数的默认值必须在声明处、常成员函数则必须在定义和声明处同时加上const。

  析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源,它在每次删除所创建的对象时执行。

可见构造函数与析构函数表面上是一个~的差别,其实作用恰恰相反,一个生成,一个回收。

class Test
{
public:
    Test(int i = 15);	// 构造函数 在创建对象时执行 构造函数可以重载
    Test(const Test &obj);	// 拷贝构造函数
    ~Test();			// 析构函数 在对象销毁时执行
    void prin(void);
private:
    int a;
};

构造函数(包括拷贝构造)的三种调用方法:

  • 括号法

    • Person p1; //默认构造函数调用
    • Person p2(10); //有参构造函数
    • Person p3(p2); //拷贝构造函数
    • 调用默认构造函数时,不要加(),若添加会被编译器认为是一个函数的声明
  • 显示法

    • Person p1; //默认构造函数调用
    • Person p2 = Person(10); //有参构造函数
    • Person p3 = Person(p2); //拷贝构造函数
    • 形如Person(10)的对象,称为匿名对象,当前行执行后系统会自动回收
  • 隐式转换法

    • Person p4 = 10; //有参构造函数
    • Person p5 = p4; //拷贝构造函数

拷贝构造函数是一种特殊的构造函数 ,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。

class Array
{
public:
    Array(const Array &obj)
    {
        std::cout << __func__ <<" : "<< __LINE__ << std::endl;
    }
    
    ...
        
private:
    ...
}

拷贝构造函数调用的三种形式

  • 一个对象作为函数参数,以值传递的方式传入函数体;

    void Array::test(Array a) //定义的类的值传递函数
    {
    	return;
    }
    

    会在调用类的值传递函数时调用复制构造函数。


  • 一个对象作为函数返回值,以值传递的方式从函数返回;

    Array Array::test(Array a)	//定义的以类为返回值的函数
    {
    	return a;
    }
    //会调用2次复制构造函数
    

  • 一个对象用于给另外一个对象进行初始化(常称为复制初始化)。

    Array obj;
    Array tmp = obj; //调用拷贝构造函数
    Array temp(obj); //调用拷贝构造函数
    

总结:当某对象是按值传递时(无论是作为函数参数,还是作为函数返回值),编译器都会先建立一个此对象的临时拷贝,而在建立该临时拷贝时就会调用类的拷贝构造函数。

  如果在类中没有定义拷贝构造函数,编译器会自行定义一个。**如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数。**该构造函数完成对象之间的位拷贝。(位拷贝又称浅拷贝)自定义拷贝构造函数是一种良好的编程风格,它可以阻止编译器形成默认的拷贝构造函数,提高源码效率。

在某些情况下必须将拷贝构造函数的形参列表中加上const:

编译失败的原因在于,编译器创建的这个临时对象,不能绑定给non-const的引用。因为修改一个编译器创建的临时对象是完全没有意义的,这个临时对象随时都会被析构掉。

// 有如下报错:
cannot bind non-const lvalue reference of type ‘demo<int>&’ to an rvalue of type ‘demo<int>// 不能将' demo<int>& '类型的非常量左值引用绑定到' demo<int> '类型的右值
/**************************************************************
 * File Name     : friend_template.cpp
 * Creator       : 程云浩
 * QQ            : 1877090085
 * Email         : daylong00@126.com
 * Creat Time    : 2022年11月23日 星期三 16时09分00秒
 * 备注          :
 ***************************************************************/
#include <iostream>
using namespace std;

// #define _CONST_TEST

template <typename T>
class demo
{
	template <typename F>
	friend demo<F> operator+(demo<F> &a, demo<F> &b);

public:
	demo(T var) : var(var) { cout << "\t" << __LINE__ << endl; }
	demo( demo<T> &var) : var(var.var) { cout << "\t" << __LINE__ << endl; }
	~demo() { ; }
	void print() const;
	void setvar(T var);

private:
	T var;
};
int main(int argc, char *argv[])
{
	demo<int> a(10), b(20);

#ifndef _CONST_TEST	// 经过测试发现未加 const 的拷贝构造 只能通过这种办法通过编译
	demo<int> c(0);
	c = a + b;
#else
	demo<int> c = a + b;
#endif

	c.print();
	c.setvar(15);
	c.print();
	return 0;
}
template <typename T>
void demo<T>::print() const
{
	cout << this->var << endl;
}

template <typename T>
void demo<T>::setvar(T var)
{
	this->var = var;
}

template <typename F>
demo<F> operator+(demo<F> &a, demo<F> &b)
{
#ifndef _CONST_TEST
	demo<F> c(a.var + b.var);
	return c;
#else
	return demo<F>(a.var + b.var);
#endif
}

默认拷贝构造函数

仅仅使用“老对象”的数据成员的值对“新对象”的数据成员逐一进行赋值。

对于一个类X,如果一个构造函数的第一个参数是下列之一:

  • X&
  • const X&
  • volatile X&
  • const volatile X&

且没有其他参数 或 其他参数都有默认值,那么这个函数是拷贝构造函数。

浅拷贝

  所谓浅拷贝,指的是 在对象复制时只对对象中的数据成员进行简单的赋值 ,默认拷贝构造函数执行的也是浅拷贝。

但是一旦对象存在了动态成员,那么浅拷贝就会出问题:

lass Rect
{
public:
	Rect() // 构造函数,p指向堆中分配的一空间
	{
		p = new int(100);
	}
	~Rect() // 析构函数,释放动态分配的空间
	{
		if (p != NULL)
			delete p;
	}
private:
	int width;
	int height;
	int *p; // 一指针成员
};

int main()
{
	Rect rect1;
	Rect rect2(rect1); // 复制对象
	return 0;
}

  问题在于在默认的复制构造函数函数中,新对象的p指针的指向没有指向new的一个新的空间,而是指向了被拷贝的的p指针。在 delete 时会因释放两次同一片空间时,发生错误。

深拷贝

对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间。


3.4 this指针

thisC++ 中的一个关键字,也是一个 const 指针,它指向当前对象,通过它可以访问当前对象的所有成员。

  • this 只能用在类的内部,通过 this 可以访问类的所有成员,包括 privateprotectedpublic 属性的。
  • 注意,this 是一个指针,要用 -> 来访问成员变量或成员函数。

this指针用来解决 局部变量 和 类的对象 同名的问题


3.5 static成员

  我们可以使用 static 关键字来把类成员定义为静态的。当我们声明类的成员为静态时,这意味着无论创建多少个类的对象,静态成员都只有一个副本。

  静态成员在类的所有对象中是共享的 。如果不存在其他的初始化语句,在创建第一个对象时,所有的静态数据都会被初始化为零。我们不能把静态成员放置在类的定义中,但是可以在类的外部通过使用范围解析运算符 :: 来重新声明静态变量从而对它进行初始化。

#include <iostream>
using namespace std;
 
class Box
{
   public:
      static int objectCount;
      // 构造函数定义
      Box(double l=2.0, double b=2.0, double h=2.0)	//内联函数 构造函数 共有
      {
         cout <<"Constructor called." << endl;
         length = l;
         breadth = b;
         height = h;
         // 每次创建对象时增加 1
         objectCount++;
      }
      double Volume()
      {
         return length * breadth * height;
      }
   private:
      double length;     // 长度
      double breadth;    // 宽度
      double height;     // 高度
};
 
// 初始化类 Box 的静态成员
int Box::objectCount = 0;
 
int main(void)
{
   Box Box1(3.3, 1.2, 1.5);    // 声明 box1
   Box Box2(8.5, 6.0, 2.0);    // 声明 box2
 
   // 输出对象的总数
   cout << "Total objects: " << Box::objectCount << endl;
 
   return 0;
}

输出如下

Constructor called.
Constructor called.
Total objects: 2

   静态成员函数即使在类对象不存在的情况下也能被调用,静态函数只要使用类名加范围解析运算符::就可以访问。如果把函数成员声明为静态的,就可以把函数与类的任何特定对象独立开来。

静态成员函数只能访问静态数据成员,不能访问其他非静态成员。


3.6 const成员

const成员变量:在初始化后不可被修改,可以采用列表初始化就地初始化对其进行初始化。

class Book
{
public:
	Book( int s );
private:
	int i;
	const int j;
    // const int j = 0;	就地初始化
	int &k;
};
// 正确示例
Book::Book( int s ): i(s), j(s), k(s){} //使用初始化列表进行初始化
// 错误示例
Book::Book( int s ){	//不能在构造函数体内初始化。
	i = s;
	j = s;
	k = s;
}

静态成员常量的初始化

class Book
{
public:
	...
private:
	static const int a = 10;	// 仅可采用就地初始化进行初始化
};

const成员函数

  • 定义:必须在成员函数的声明和定义处同时加上 const 关键字。(eg:void print() const
  • 目的:常成员函数不能随意修改普通成员变量,以保护对象数据。
  • 使用: 不能 在**const成员函数中调用const成员函数**。
  • 重载:常成员函数可以与普通函数同时存在,同名函数,可以看作重载。

const成员函数可以随意修改或调用static成员和mutable成员。

mutable的意思是"可变的",和const的意思正好相反,其作用也是解除常函数无法修改成员变量的限制。

常对象只能调用常函数


3.7 匿名对象2

  1. C++中的匿名对象是pure RValue, 因而不能作为引用传进去。
  2. 匿名对象只存在于构造该对象的那行代码,离开构造匿名对象的哪行代码后立即调用析构函数。

产生匿名对象的三种情况:

  1. 以值的方式给函数传参;
  2. 类型转换;
  3. 函数需要返回一个对象时;return temp;
//匿名对象产生的三种场景
#include <iostream>
using namespace std;

class Point
{
public:
    Point(int a, int b)
    {
        cout << "有参构造函数被调用了1" << endl;
        this->x = a;
        this->y = b;
    }
    Point(const Point &a1)
    {
        cout << "拷贝构造函数被调用了2" << endl;
        this->x = a1.x;
        this->y = a1.y;
    }
    ~Point()
    {
        cout << "析构函数被调用了3" << endl;
        cout << "x=" << x << endl;
        cout << "y=" << y << endl;
    }
    Point Protset(int a)
    {
        this->x = a;
        return *this;
        //执行 return *this; 会产生一个匿名对象,作为返回值
        //强调:如果返回值是引用,则不会产生匿名对象
    }
    Point Protset2(int a)
    {
        Point temp(a, a);
        return temp;
        //执行 return temp;会先产生一个匿名对象,执行拷贝构造函数,作为返回值,
        //然后释放临时对象temp
    }
    //总结:函数返回值为一个对象(非引用)的时候会产生一个匿名对象,匿名对象根据主函数的操作决定生命周期

    Point &Protset3(int a)
    {
        Point temp(a, a);
        return temp;
        //执行 return temp;不会产生匿名对象,而是会将temp的地址先赋值到引用中,
        //在释放temp的内存,此时Point&得到是一个脏数据
    }
    void PrintfA() const
    {
        cout << "x=" << x << endl;
        cout << "y=" << y << endl;
    }
private:
    int x;
    int y;
};

void ProtectA()
{
    //生成一个匿名对象,因为用来初始化另一个同类型的对象,这个匿名对象会直接转换成新的对象,
    //减少资源消耗
    Point p1 = Point(1, 1);
    /*Point p2(2, 2);
    p2 = p1.Protset(3);
    p2.PrintfA();*/
    //观察发现p2打印出正确数据,因此得出结论p1.Protset(3);返回来一个匿名对象,
    //但是这个匿名对象执行完"="之后,才会被释放
    Point p4(5, 5);
    p4 = p1.Protset2(4);
    p4.PrintfA();
}

int main()
{
    ProtectA();
    return 0;
}

匿名对象的生命周期

#include <iostream>

using namespace std;

class Cat
{
public:
    Cat(int a = 0) : var(a) { cout << "Cat类 无参构造函数" << endl; }
    Cat(const Cat &obj) : var(obj.var) { cout << "Cat类 拷贝构造函数" << endl; }
    ~Cat() { cout << "Cat类 析构函数 " << endl; }
    int var;
};

void playStage() //一个舞台,展示对象的生命周期
{
    Cat(); /*在执行此代码时,利用无参构造函数生成了一个匿名Cat类对象;
    执行完此行代码,因为外部没有接此匿名对象的变量,此匿名又被析构了*/
    cout << "aa" << endl;
    Cat cc = Cat(); /*在执行此代码时,
    利用无参构造函数生成了一个匿名Cat类对象;然后将此匿名变 成了cc这个实例对象*/
    cout << cc.var << endl;
}

int main()
{
    playStage();
    return 0;
}
// 输出
    Cat类 无参构造函数
    Cat类 析构函数 
    aa
    Cat类 无参构造函数
    0
    Cat类 析构函数

😉四、友元3

类的友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用

只有在类声明中才能够决定那一个函数是本类的友元函数,因此类声明仍然控制了那些函数可以访问私有成员。

友元函数包括3种:

  • 友元函数
  • 友元类
  • 友元成员函数

优点:

  1. 可以灵活地实现需要访问若干类的私有或受保护的成员才能完成的任务;
  2. 便于与其他不支持类概念的语言(如C语言、汇编等)进行混合编程;
  3. 通过使用友元函数重载可以更自然地使用C++语言的IO流库。

缺点:

一个类将对其非公有成员的访问权限授予其他函数或者类,会破坏该类的封装性,降低该类的可靠性和可维护性。


4.1 类的友元函数

  • 用法:全局函数写到类中任意位置作声明,并且最前面写关键字 friend
    • 友元函数不能const修饰;
    • 一个函数可以是多个类的友元函数;
    • 友元函数的定义在类体外实现,不需要加类限定
    • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  • 作用:友元函数可访问类的所有成员,但不是类的成员函数。
  • 原理:友元函数的调用与普通函数的调用和原理相同。

为什么需要友元函数?

  • 提供一个对外(除了他自己以外)访问的窗口;
  • 两个类要共享数据的时候,友元函数可以减少系统开销,调高效率;
  • 运算符重载的某些场合需要使用友元函数;
  • 通过友元函数,虚继承和私有构造函数可以让一个类不被继承;
#include <iostream>
using namespace std;
class A
{
public:
    friend void set_show(int x, A &a);      //该函数是友元函数的声明 放到类里面任何位置都可以,
    //一般建议放到类的开始或是结束
private:
    int data;
};
void set_show(int x, A &a)  //友元函数定义,为了访问类A中的成员
{
    a.data = x;
    cout << a.data << endl;
}
int main(void)
{
    class A a;
    set_show(1, a);
    return 0;
}

4.3 友元成员函数

firent 函数不仅可以定义一个普通函数(非成员函数),而且,可以是另一个类中的成员函数。

作用:只在某个成员函数中可以访问 另一个类的所有成员变量和成员函数。

使 类B中的成员函数 成为 类A的友元函数,这样 类B的该成员函数 就可以访问 类A的所有成员 了。

class student; //提前声明student类;
// 一个类在使用之前,没有被定义,而是放在后面定义,那么,我们可以在使用之前先声明它。

class my_print //定义 my_print 类;
{
public:
	void print(student &s); //声明函数;
};
class student
{
private:			  //定义私有类型成员变量
	char name[32];	  //姓名
	char addr[32];	  //家庭地址
	long long number; //电话号码
public:				  //以下部分是公有部分
	student(char *pn, char *pa, long long n)
	{
		strcpy(name, pn);
		strcpy(addr, pa);
		number = n;
	}
	friend void my_print::print(student &s); //声明友元函数;
};

  在该例子中,类B必须先定义,否则类A就不能将一个B的函数指定为友元。然而,只有在定义了类A之后,才能定义类B的该成员函数。更一般的讲,必须先定义包含成员函数的类,才能将成员函数设为友元。另一方面,不必预先声明类和非成员函数来将它们设为友元。


4.4 友元类

友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。当希望一个类可以存取另一个类的私有成员时,可以将该类声明为另一类的友元类。

如果你是我的友元类,那么你就可以在你的成员函数中访问我的所有成员(包括私有成员变量和函数)

注意:

  1. 友元关系不能被继承。

  2. 友元关系是单向的,不具有交换性。

    若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。

  3. 友元关系不具有传递性。

    若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明。

#include <iostream>
using namespace std;
class A
{
public:
    friend class C;   // 这是友元类的声明  声明C为A的友元类
private:
    int data;
};

class C             // 友元类定义,为了访问类A中的成员
{
public:
    void set_show(int x, A &a) { a.data = x; cout<<a.data<<endl;}
};

int main(void)
{
    class A a;
    class C c;
    c.set_show(1, a);
    return 0;
}

🤭五、运算符重载4

运算符重载的本质为函数重载,但有一定的规则需要遵循。

运算符重载的形式:<返回类型说明符> operator<要重载的运算符> <参数列表>

实现运算符重载的方式:

  1. 类成员函数;
  2. 友元函数;
  3. 普通函数;

以上三种重载方式,区别在于可访问类成员的权限不同。

可以把运算符重载函数声明为类的友元函数,这样就可以不用创建对象而直接调用函数。

为了在未创建对象操作

5.1 一元运算符重载

以下例子的析构函数和构建函数原型:

demo::demo(int tmp) : var(tmp)
{
	cout << __func__ << " : " << __LINE__ << endl;
}

demo::demo(const demo &a) : var(a.var)
{
	cout << __func__ << " : " << __LINE__ << endl;
}

demo::~demo()
{
	cout << __func__ << " : " << __LINE__ << endl;
}

以成员函数重载 前++ 和 后++

class demo
{
public:
	demo(int tmp = 0);
	demo(const demo &a);
	~demo();
	demo& operator++(void)	//前++
	{
		++(this->var);
		return *this;
	}
	demo operator++(int)	//后++
	{
		demo tmp(this->var++);
		return tmp;
	}
private:
	int var;
};

以友元函数重载 前++ 和 后++

class demo
{
	friend demo& operator++(demo &a);		//前++
    friend demo operator++(demo &a, int);	//后++
public:
	demo(int var);
	~demo();
private:
	int var;
};
demo& operator++(demo &a)	//前++
{
    a.var++;
    return a;
}
demo operator++(demo &aa, int)	//后++
{
	demo tmp(aa.var++);
	return tmp;
}

区分前置递增运算符和后置递增运算符的办法是:用形参列表是否带int类型,带int类型则是后置递增/递减运算符

5.2 二元运算符

加运算符(+)、减运算符(-)、乘运算符(*)、除运算符(/)。

友元函数 重载 +

class demo
{
	friend demo operator+(demo &a, demo &b);
public:
	demo(int tmp = 0);
	demo(const demo &a);
	~demo();
private:
	int var;
};
demo operator+(demo &a, demo &b)
{
	demo tmp(a.var + b.var);
	return tmp;
}

成员函数 重载 +

class demo
{
public:
	demo(int tmp = 0);
	demo(const demo &a);
	~demo();
	demo operator+(demo &a);
private:
	int var;
};
demo demo::operator+(demo &a)
{
	demo tmp(this->var + a.var);
	return tmp;
}

5.3 赋值运算符重载

C++ 允许重载赋值运算符(=),用于创建一个对象,比如拷贝构造函数。

#include <iostream>
using namespace std;

class demo
{
	friend std::ostream &operator<<(std::ostream &out, demo &dat);

public:
	demo(int tmp = 0) : var(tmp) {}
	demo(const demo &a) {}
	~demo() {}
	void operator=(const demo &r)
	{
		var = r.var;
	}

private:
	int var;
};
std::ostream &operator<<(std::ostream &out, demo &dat)
{
	out << dat.var;
	return out;
}
int main(int argc, char *argv[])
{
	demo a(15), b(20);
	a = b;
	cout << a << endl;
	return 0;
}

5.4 重载 << 运算符5

实际上,它已经被重载过很多次了。最初,<< 运算符是 C++ 的左移运算符。ostream 类对该运算符进行了重载,将其转换为一个输出工具。

cout 是一个 ostream 类的对象,它是智能的,能够识别所有的 C++ 基本类型,这是因为对于每种基本类型,ostream 类声明中都包含了相应的重载的 operator<<() 成员函数。因此,要是 cout 能够识别自定义对象,一种方法是将一个新的函数运算符定义添加到 ostream 类声明中,但修改 iostrea 文件是一个危险的注意,这样做会在标准接口上浪费时间。

相反,应该让自定义类声明来让该类知道如何使用 cout —— 在 自定义类 中定义一个友元函数。

#include <iostream>
using namespace std;

class demo
{
	friend std::ostream &operator<<(std::ostream &out, demo &dat);
public:
	demo(int tmp = 0): var(tmp){}
	demo(const demo &a): var(a.var){}
	~demo(){}
private:
	int var;
};
std::ostream &operator<<(std::ostream &out, demo &dat)
{
	out << dat.var;
	return out;
}
int main(int argc, char *argv[])
{
	demo a(15);
	cout << a << endl;
	return 0;
}

5.5 函数调用运算符重载

C++ 允许重载函数调用运算符(即 () 符号)。重载 () 的目的不是为了创造一种新的调用函数的方式,而是创建一个可以传递任意个参数的运算符函数。其实就是创建一个可调用的对象。

#include <iostream>
using namespace std;

class Rect
{
public:
    Rect()
    {
        width = 0;
        height = 0;
    }

    Rect(int a, int b)
    {
        width = a;
        height = b;
    }

    void operator()()
    {
        cout << "Area of myself is:" << width * height << endl;
    }

private:
    int width;
    int height;
};

int main()
{
    Rect r1(3, 4), r2(6, 8);

    cout << "r1: ";
    r1();

    cout << "r2: ";
    r2();

    return 0;
}

5.6 下标运算符重载

下标操作符([])通常用于访问数组元素。C++ 允许重载下标运算符用于增强操作 C++ 数组的功能,重载下标运算符最重要的作用就是设置一个哨兵,防止数组访问越界。

#include <iostream>
using namespace std;

const int SIZE = 10;

class Fibo
{
private:
    // 偷懒,防止把 SIZE 设置的过小
    int arr[SIZE + 3];

public:
    Fibo()
    {
        arr[0] = 0;
        arr[1] = 1;

        for (int i = 2; i < SIZE; i++)
        {
            arr[i] = arr[i - 2] + arr[i - 1];
        }
    }

    int &operator[](unsigned int i)
    {
        if (i >= SIZE)
        {
            throw "(索引超过最大值) ";
        }
        return arr[i];
    }
};

int main()
{
    Fibo fb;
    try
    {
        //保护代码
        for (int i = 0; i < SIZE + 1; i++)
        {
            cout << fb[i] << " ";
        }
    }
    catch (const char *msg) //捕获异常
    {
        //处理异常
        cerr << msg << endl;
    }
    cout << endl;

    return 0;
}

5.7 类成员访问运算符重载

C++ 允许重载类成员访问运算符(->),用于为一个类赋予 “指针” 行为。

重载 -> 运算符时需要注意以下几点:

  • 运算符 -> 必须是一个成员函数;
  • 如果使用了 -> 运算符,返回类型必须是指针或者是类的对象;
  • 运算符 -> 通常与指针引用运算符 * 结合使用,用于实现智能指针的功能;
  • 这些指针是行为与正常指针相似的对象,唯一不同的是,通过指针访问对象时,它们会执行其它的任务(比如,当指针销毁时,或者当指针指向另一个对象时,会自动删除对象)。
#include <iostream>
#include <vector>

using namespace std;

// 假设一个实际的类
class Obj
{
    static int i, j;

public:
    void f() const { cout << i++ << endl; }
    void g() const { cout << j++ << endl; }
};

// 静态成员定义
int Obj::i = 10;
int Obj::j = 12;

// 为上面的类实现一个容器
class ObjContainer
{
    std::vector<Obj *> a;

public:
    void add(Obj *obj)
    {
        a.push_back(obj); // 调用向量的标准方法
    }

    friend class SmartPointer;
};

// 实现智能指针,用于访问类 Obj 的成员
class SmartPointer
{
    ObjContainer oc;
    int index;

public:
    SmartPointer(ObjContainer &objc)
    {
        oc = objc;
        index = 0;
    }
    // 前缀版本
    // 返回值表示列表结束
    bool operator++()
    {
        if (index >= oc.a.size())
            return false;

        if (oc.a[++index] == 0)
            return false;

        return true;
    }
    // 后缀版本
    bool operator++(int)
    {
        return operator++();
    }

    // 重载运算符 ->
    Obj *operator->() const
    {
        if (!oc.a[index])
        {
            std::cout << "Zero value";
            return (Obj *)0;
        }

        return oc.a[index];
    }
};

int main()
{
    const int sz = 6;

    Obj o[sz];
    ObjContainer oc;

    for (int i = 0; i < sz; i++)
    {
        oc.add(&o[i]);
    }

    SmartPointer sp(oc);

    do
    {
        sp->f();
        sp->g();
    } while (sp++);

    return 0;
}

5.8 逻辑非运算符重载

逻辑非(!)运算符是一元运算符,它总是出现在所操作对象的左边,如 !obj。逻辑非运算符运算符返回的类型为 bool 类型,当对象为真值时,返回假;当对象为假值时,返回真。

#include <iostream>
using namespace std;

class Rect
{
private:
    int width;
    int height;

public:
    Rect()
    {
        width = 0;
        height = 0;
    }

    Rect(int a, int b)
    {
        width = a;
        height = b;
    }

    int area() { return width * height; }

    // 当 width 或者 height 有一个小于 0 则返回 true
    bool operator!()
    {
        if (width <= 0 || height <= 0)
            return true;
        return false;
    }
};

int main()
{
    Rect r1(3, 4), r2(-3, 4);

    if (!r1)
        cout << "r1 is not a rectangle" << endl;
    else
        cout << "r1 is a rectangle" << endl;

    if (!r2)
        cout << "r2 is not a rectangle" << endl;
    else
        cout << "r2 is a rectangle" << endl;

    return 0;
}

🤒六、模板

泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。

模板
函数模板
类模板

6.1 函数模板

函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生 函数的特定类型版本。

6.1.1 函数模板格式

template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名 ( 参数列表 ){}

typename 是用来定义模板参数 关键字也可以使用 class( 切记:不能使用 struct 代替class)。

函数模板原理

函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。 所以其实模板就是将本来应该我们做的重复的事情交给了编译器。

简单说就是本来我们应该多去写的Swap的重复工作去给编译器做了

6.1.2 函数模板的实例化

函数模板的实例化
隐式实例化
让编译器根据 实参推演 模板参数的实际类型
显式实例化
在函数名后的 <> 中 指定 模板参数的实际类型

6.1.3 模板的匹配原则

  1. 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数

    就比如一个模板的Add和一个自己实现的Add可以一起存在

    void Test ()
    {
       Add ( 1 , 2 );    // 与非模板函数匹配,编译器不需要特化
       Add < int > ( 1 , 2 );  // 调用编译器特化的 Add 版本
    }
    
  2. 对于非模板函数和同名函数模板,如果其他条件都相同,在调用时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数,那么将选择模板。

    简单说会先找自己实现的有没有,没有就去看模板能不能实例化一个。

  3. 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换。

6.1.4 声明定义分离

template <class T>
T add(T a, T b);

int main(int argc, char *argv[])
{
	cout << add<int>(48, 4) << endl;
	cout << add<char>(48, 4) << endl;
	return 0;
}

template <class T>	// 模板参数声明定义都要给
T add(T a, T b)
{
	T c = a + b;
	return c;
}

// 输出
52
4

6.1.5 函数模板的特化

  所谓特化,就是将泛型的东西搞得具体化一些,从字面上来解释,就是为已有的模板参数进行一些使其特殊化的指定,使得以前不受任何约束的模板参数,或受到特定的修饰(例如const或者摇身一变成为了指针之类的东东,甚至是经过别的模板类包装之后的模板类型)或完全被指定了下来。

作用:当函数模板需要对某些类型进行特化处理,称为函数模板的特化。

特化分为全特化和偏特化

  1. 全特化

就是模板中模板参数全被指定为确定的类型。

全特化也就是定义了一个全新的类型,全特化的类中的函数可以与模板类不一样。

  1. 偏特化

就是模板中的模板参数没有被全部确定,需要编译器在编译时进行确定。

  全特化的标志就是产生出完全确定的东西,而不是还需要在编译期间去搜寻适合的特化实现,貌似在我的这种理解下,全特化的 东西不论是类还是函数都有这样的特点。

函数模版特化:目前的标准中,模板函数只能全特化,没有偏特化

#include <iostream>
#include <cstring>
#include <cmath>
#include "tt.h"
// general version
template <class T>
class Compare
{
public:
    static bool IsEqual(const T &lh, const T &rh)
    {
        std::cout << "in the general class..." << std::endl;
        return lh == rh;
    }
};

// specialize for float
template <>
class Compare<float>
{
public:
    static bool IsEqual(const float &lh, const float &rh)
    {
        std::cout << "in the float special class..." << std::endl;

        return std::abs(lh - rh) < 10e-3;
    }
};

// specialize for double
template <>
class Compare<double>
{
public:
    static bool IsEqual(const double &lh, const double &rh)
    {
        std::cout << "in the double special class..." << std::endl;

        return std::abs(lh - rh) < 10e-6;
    }
};

int main(void)
{
    Compare<int> comp1;
    std::cout << comp1.IsEqual(3, 4) << std::endl;
    std::cout << comp1.IsEqual(3, 3) << std::endl;

    Compare<float> comp2;
    std::cout << comp2.IsEqual(3.14, 4.14) << std::endl;
    std::cout << comp2.IsEqual(3, 3) << std::endl;

    Compare<double> comp3;
    std::cout << comp3.IsEqual(3.14159, 4.14159) << std::endl;
    std::cout << comp3.IsEqual(3.14159, 3.14159) << std::endl;

    std::cout << hh<string>()("11") << std::endl;
    system("pause");
    return 0;
}

6.2 类模板

6.2.1 格式

template<class T1, class T2, ..., class Tn>
class 类模板名
{
    // 类内成员定义
};

6.2.2 类模板的实例化

  类模板实例化与函数模板实例化不同, 类模板实例化需要在类模板名字后跟 <> ,然后将实例化的 类型放在 <> 中即可,类模板名字不是真正的类,而实例化的结果才是真正的类

#include <iostream>
using namespace std;

template <typename T>
class demo
{
public:
	demo(T tmp) : var(tmp)	{ ; }	// 不能给模板参数指定默认值
	demo(demo<T> &tmp);
	~demo()	{ ; }
	void print() const;

private:
	T var;
};

int main(int argc, char *argv[])
{
	demo<int> a(150);	//将类模板示例化为int类
	a.print();
	return 0;
}
// 在类外部定义时需要在每个函数前都需要加模板参数列表。
template <typename T>
demo<T>::demo(demo<T> &tmp) : var(tmp.var)
{
	;
}
template <typename T>
void demo<T>::print() const
{
	cout << this->var << endl;
}

6.2.3 非类型形参

模板参数
类型形参
非类型形参
以class或typename 后的参数类型
非类型模板参数必须是 整型及相关类型,指针,引用
#include <iostream>
#include <cassert>
#include <cstring>

#define DEFAULT

using namespace std;

#ifdef DEFAULT
template <typename T = int, int len = 10> // 可以为模板参数指定默认值
// template <typename T, int len = 10>	  // 或者这样
#elif
template <typename T, int len>
#endif
class demo
{
public:
	demo(T tmp)
	{
		p = new T[len];
		assert(nullptr != p);	// 如果断言为false,则中止程序。
		bzero(p, len);
	}
	demo(demo<T, len> &tmp);
	~demo()
	{
		delete[] this->p;
	}
	void print() const;
	int setvalue(int pos, T var);

private:
	T *p;
};

int main(int argc, char *argv[])
{
	demo<int, 15> a(10);
	a.print();
	for (int i = 0; i < 15; i++)
	{
		a.setvalue(i, i + 1);
	}
	a.print();
	return 0;
}

template <typename T, int len>
demo<T, len>::demo(demo<T, len> &tmp)
{
	this->p = new T[len];
	assert(!this->p);
	for (int i = 0; i < len; i++)
	{
		this->p[i] = tmp.p[i];
	}
}

template <typename T, int len>
void demo<T, len>::print() const
{
	for (int i = 0; i < len; i++)
		cout << this->p[i] << " ";
	cout << endl;
}

template <typename T, int len>
int demo<T, len>::setvalue(int pos, T var)
{
	assert(pos < len);
	this->p[pos] = var;
	return var;
}

6.2.4 类模板友元重载运算符

#include <iostream>
using namespace std;
template <typename T>
class demo
{
	template <typename F>
	friend demo<F> operator+(demo<F> &a, demo<F> &b);

public:
	demo(T var) : var(var) { ; }
	demo(const demo<T> &var) : var(var.var) { ; }
	~demo() { ; }
	void print() const;
	void setvar(T var);

private:
	T var;
};
int main(int argc, char *argv[])
{
	demo<int> a(10), b(20);
	demo<int> c(a + b);
	c.print();
	c.setvar(15);
	c.print();
	return 0;
}
template <typename T>
void demo<T>::print() const
{
	cout << this->var << endl;
}

template <typename T>
void demo<T>::setvar(T var)
{
	this->var = var;
}

template <typename F>
demo<F> operator+(demo<F> &a, demo<F> &b)
{
	return demo<F>(a.var + b.var);
}

😭七、类的继承

C++中,继承是面向对象的一个重要概念,是C++语言的一种重要机制。我们通过继承现有的类来创建一个新的类。

目的:继承是为了提高代码的重用性提高执行效率

使用:class 派生类 : <访问限定符> 基类

如果A类继承B类,那么A类将拥有B类的所有的属性和方法。

继承方式publicprotectedprivate
公有继承publicpublicprotected不能直接访问
保护继承protectedprotectedprotected不能直接访问
私有继承privateprivateprivate不能直接访问
1.is-a关系
	一个类是另外一个类的一种,此时两个类之间的关系is-a关系
	即 is-a用来描述继承,满足is-a关系的两个类才能够继承
2.has-a关系
	当一个类中包含另外一个类的时候就用has-a描述;

7.1 多重继承

  多重继承 (multiple inheritance): 一个派生类有两个或多个基类, 派生类从两个或多个基类中继承所需的属性.。C++ 为了适应这种情况,允许一个派生类同时继承多个基类.。这种行为称为多重继承。

优点

  • 自然地做到了对单继承的扩展
  • 可以继承多个类的功能

缺点

  • 结构复杂化
  • 优先顺序模糊
  • 功能冲突

多重继承的语法:

class D: public A, private B, protected C
    // 他们的构造顺序是按":"后声明的先后顺序决定
{
    // 类D新增加的成员
}

多重继承的构造函数:

D(形参列表): A(实参列表), B(实参列表), C(实参列表){
   // 派生类中新增数成员据成员初始化语句
}

7.2 多重继承的命名冲突

  当两个或多个基类中有同名的成员时,如果直接访问该成员,就会产生命名冲突,编译器不知道使用哪个基类的成员。这个时候需要在成员名字前面加上类名和域解析符::,以显式地指明到底使用哪个类的成员,消除二义性。

7.3 虚继承

在继承方式前面加上 virtual 关键字就是虚继承。

目的:消除菱形继承带来的二义性和空间浪费。

class Basic
class A
class B
class C

比如一个类C通过继承类A和类B,但是类A和类B又同时继承于公共基类。

  这种继承方式也存在数据的二义性,这里的二义性是由于他们间接都有相同的基类导致的。 这种菱形继承除了带来二义性之外,还会浪费内存空间

  类C中存在两份的基类,分别存在类A和类B中,如果数据多则严重浪费空间,也不利于维护, 我们引用基类中的数据还需要通过域运算符进行区分。


🤫八、多态

多态: 简而言之,多种状态。 同一接口(函数),面向不同对象,效果不同。

C++的多态必须满足两个条件:
1 必须通过基类的指针或者引用调用虚函数
2 被调用的函数是虚函数,且必须完成对基类虚函数的重写

  • 静态多态

    cout, 函数重载,运算符重载, 模板技术,编译时已经确定了调用的版本(早绑定)

  • 动态多态

    动态绑定,运行时才确定是哪个版本(晚绑定)。

    动态也就是我们常说的多态,动态多态是在运行中实现的。根据父类的指针或引用接收不同对象,来确定自己会调用哪个类的虚函数。

实现动态多态的关键点

  1. 继承
  2. 子类重写父类方法
  3. 设计一个函数接口(接受不同的对象)

8.1 虚函数6

virtual修饰的类成员函数称为虚函数

虚函数使用的其核心目的是通过基类访问派生类定义的函数。所谓虚函数就是在基类定义一个未实现的函数名,为了提高程序的可读性,建议后代中虚函数都加上virtual关键字。

class base
{
public:
	base();
	virtual void test(); //定义的一个虚函数
private:
	char *basePStr;
};

上述代码在基类中定义了一个test的虚函数,所以可以在其子类重新定义父类的做法这种行为成为覆盖或重写。

如果不希望某个类被继承,或不希望某个虚函数被重写

class Base {
public:
    virtual void Show(int x) final; // 虚函数 
};
 
class Derived : public Base {
public:
    virtual void Show(int x) override; // 重写提示错误  
};

则可以在类名和虚函数后加上 final 关键字,加上 final 关键字后,再被继承或重写,编译器就会报错。

8.1.1 纯虚函数

  纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加 =0:

virtual void funtion1()=0;

定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。

注意:
  定义一个函数为虚函数,不代表函数为不被实现的函数。定义他为虚函数是为了允许用基类的指针来调用子类的这个函数
  定义一个函数为纯虚函数,才代表函数没有被实现。定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。
1.普通的函数不能是虚函数,只能是类中的函数。

2.静态成员函数不能加virtual


8.1.2 虚析构

虚析构就是析构函数为虚函数。

虚析构函数的作用:为了避免内存泄漏,而且是当子类中会有指针成员变量时才会使用到。

C++中开发中,基类中的析构函数一般都要定义成虚函数。

#include <iostream>
using namespace std;

class Base // 基类
{
public:
	Base(){};
	virtual ~Base() { cout << "delete Base\n"; }; // 基类的析构函数是虚函数!
	virtual void DoSomething() { cout << "Do Something in class Base!\n"; };
};

class Derived : public Base // 派生类
{
public:
	Derived(){};
	~Derived() { cout << "delete Derived\n"; };
	void DoSomething() { cout << "Do Something in Derived\n"; };
};

int main()
{
	Base *b = new Derived;
	b->DoSomething(); // 重写DoSomething函数
	delete b;
	return 0;
}
  1. 普通析构函数

      当基类中的析构函数没有声明为虚析构函数时,派生类开始从基类继承,基类的指针指向派生类的对象时,delete基类的指针时,只会调用基类的析构函数,不会调用派生类的析构函数。

  2. 虚析构函数

      当基类中的析构函数声明为虚析构函数时,派生类开始从基类继承,基类的指针指向派生类的对象时,delete基类的指针时,先调用派生类的析构函数,再调用基类中的析构函数。

8.2 重载、重写、隐藏

  • 重载: 同一区域,函数名相同,参数类型、参数个数不同,与返回值无关。

    注意: 重载要点:同一区域

  • 覆盖(重写): 不同区域(基类,派生类), 函数名相同,返回相同,参数相同,+ virtual

    注意: 覆盖要点:不同区域 函数名 ,返回值,参数,virtual 缺一不可(协变除外)

  • 隐藏:

    • 不同区域(基类,派生类), 函数名相同,返回相同,参数相同

    • 不同区域(基类,派生类), 函数名相同,返回相同,参数不同 + virtual

    • 不同区域(基类,派生类), 函数名相同,返回相同,参数不同

      注意: 隐藏要点: 不同区域 名字相同,覆盖不住

子类和父类的同名函数不是重定义就是重写。

#include <iostream>
using namespace std;

class A
{
public:
	void foo() { printf("1\n"); }
	virtual void fun() { printf("2\n"); }
};
class B : public A
{
public:
	//隐藏:派生类的函数屏蔽了与其同名的基类函数
	void foo() { printf("3\n"); }
	//多态、覆盖
	void fun() { printf("4\n"); }
};
int main(void)
{
	A a;
	B b;
	A *p = &a;
	p->foo(); //输出1
	p->fun(); //输出2
	p = &b;
	p->foo(); //取决于指针类型,输出1
	p->fun(); //取决于对象类型,输出4,体现了多态
	return 0;
}

8.3 抽象类

  在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。

包含纯虚函数的类,被称为抽象类,它不能生成对象。(可以包含普通方法,包含静态方法)

  抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。

抽象类的意义:

制定了接口标准, 代码更严谨,编译器要求在派生类中必须对基类的纯虚函数予以重写以实现多态性。

例如:Animal 没有具体的特征信息,太过于“抽象”了,应该被设计成抽象类。

class Animal
{
public:
    virtual void eat(void) = 0;   //纯虚函数
};

8.4 限制构造

通过对构造函数加以限制,防止通过类直接定义对象,只能通过特殊方法获取到对象。
意义:实现特殊需求,例如单例设计模式

#include <iostream>
using namespace std;

//限制构造
//方法1: 把构造函数移动到 protected 
#if 0
class A
{
protected:
    A(){
        cout<<"A 构造"<<endl;
    }
};

class B:public A
{

};

int main(void)
{
    // A a1;
    B b1;
    return 0;
}
#endif
//方法2: 把构造函数移动到 private

class A
{
public:
    //可以通过静态方法 --- 获取到对象
    static A *get_instance(void)
    {
        A *ptr = new A;
        return ptr;
    }

    void print_val(void) const { cout << "val:" << val << endl; }
    friend A *get_obj(void);

private:
    A() { cout << "A 构造" << endl; }
    int val = 100;
};

//可以通过友元函数 --- 获取到对象
A *get_obj(void)
{
    A *ptr = new A;
    return ptr;
}

int main(void)
{
    // A a1;
    // A *ptr = get_obj();
    // ptr->print_val();

    A *ptr = A::get_instance();
    ptr->print_val();

    return 0;
}

😎九、错误处理

在 C++ 程序错误一般分类:语法错误;运行错误;语义错误(也称逻辑错误)。

  • 语法错误:编译器在编译时会检查语法错误。

    比如出现引号、逗号、分号,运算符是中文的符号。

  • 逻辑错误:只能人为调试。

    比如 if (a = 3) {....},这种粗心大意造成的输出不正常。

  • 运行错误:C++ 提供了异常(Exception)机制,让我们能够捕获运行时错误,给程序一次“起死回生”的机会,或者至少告诉用户发生了什么再终止程序。

    打开不存在的文件、下标越界,栈溢出等。

9.1 常见错误处理方法

对于运行错误:我们有 3 种方式把错误信息传递给函数的调用者。7

方法1:通过函数返回值来告知调用者是否出错。

  这种方式最大的问题是使用不便,因为函数不能直接把计算结果通过返回值赋值给其他变量,同时也不能把这个函数计算的结果直接作为参数传递给其他函数。

方法2:设置一个全局变量保存错误信息。

  此时我们可以在返回值中传递计算结果了。这种方法比第一种方法使用起来更加方便,因为调用者可以直接把返回值赋值给其他变量或者作为参数传递给其他函数。

类似错误码。

  但这个方式有个问题:调用者很容易就会忘记去检查全局变量,因此在调用出错的时候忘记做相应的错误处理,从而留下安全隐患。

方法3:异常捕获和处理。

  当函数运行出错的时候,我们就抛出一个异常,我们还可以根据不同的出错原因定义不同的异常类型。因此函数的调用者根据异常的类型就能知道出错的原因,从而做相应的处理。

C++ 异常处理涉及到三个关键字:trycatchthrow

C++出错处理思想:分离思想,将问题检测和问题处理相分离。

  • throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
  • catch: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常。
  • try: try块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个 catch 块。
#include <iostream>
using namespace std;

double division(int a, int b)
{
	if (b == 0)
	{
		//抛出异常
		throw "Division by zero condition!";
	}
	return (a / b);
}

int main()
{
	int x = 50;
	int y = 0;
	double z = 0;

	try
	{
		//保护代码
		z = division(x, y);
		cout << z << endl;
	}
	catch (const char *msg) //捕获异常
	{
		//处理异常
		cerr << msg << endl;
	}
	return 0;
}

  在执行程序发生异常时,可以不在本函数中处理,而是抛出一个错误信息,把它传递给上一级的函数来解决,上一级解决不了,再传给其上一级,由其上一级处理,直到最高一级还无法处理的话,运行系统会自动调用系统函数terminate,由它调用abort终止程序。

  这样机制就是异常引发和处理机制分离。这使得底层函数只需要解决实际的任务,而不必过多考虑对异常的处理,而把异常处理的任务交给上一层函数去处理


9.2 标准异常

#include <exception>	// 标准异常所在的头
异常描述
std::exception该异常是所有标准 C++ 异常的父类。
std::bad_alloc该异常可以通过 new 抛出。
std::bad_cast该异常可以通过 dynamic_cast 抛出。
std::bad_exception这在处理 C++ 程序中无法预期的异常时非常有用。
std::bad_typeid该异常可以通过 typeid 抛出。
std::logic_error理论上可以通过读取代码来检测到的异常。
std::domain_error当使用了一个无效的数学域时,会抛出该异常。
std::invalid_argument当使用了无效的参数时,会抛出该异常。
std::length_error当创建了太长的 std::string 时,会抛出该异常。
std::out_of_range该异常可以通过方法抛出,例如 std::vector 和 std::bitset<>::operator
std::runtime_error理论上不可以通过读取代码来检测到的异常。
std::overflow_error当发生数学上溢时,会抛出该异常。
std::range_error当尝试存储超出范围的值时,会抛出该异常。
std::underflow_error当发生数学下溢时,会抛出该异常。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4i38zi3k-1669862202497)(06_C++ 语言.assets/错误处理.png)]

#include <iostream>
#include <new>
#include <stdexcept>

using namespace std;

//异常处理
int main()
{
	string *s;
	try
	{
		s = new string("12345678901234");
		cout << s->substr(15, 5);
	}

	catch (bad_alloc &t)
	{
		cout << "Exception occurred:" << t.what() << endl;
	}

	catch (out_of_range &t)
	{
		cout << "Exception occurred:" << t.what() << endl;
	}

	return 0;
}

🤗十、智能指针8

  智能指针是存储指向动态分配(堆)对象指针的类,用于生存期的控制,能够确保在离开指针所在作用域时,自动地销毁动态分配的对象,防止内存泄露。智能指针的核心实现技术是引用计数,每使用它一次,内部引用计数加1,每析构一次内部的引用计数减1,减为0时,删除所指向的堆内存。

C++11中提供了三种智能指针,使用这些智能指针时需要引用头文件**<memory>**

  • std::shared_ptr:共享的智能指针

    共享智能指针是指多个智能指针可以同时管理同一块有效的内存

  • std::unique_ptr:独占的智能指针

  • std::weak_ptr:弱引用的智能指针,它不共享指针,不能操作资源,是用来监视shared_ptr的。

10.1 std::shared_ptr

  1. shared_ptr的初始化

    初始化有三种方式:通过构造函数、std::make_shared辅助函数以及reset方法

  2. 获取原始指针

    调用共享智能指针类提供的get()方法得到原始地址

  3. 指定删除器

  当智能指针管理的内存对应的引用计数变为0的时候,这块内存就会被智能指针析构掉了。另外,我们在初始化智能指针的时候也可以自己指定删除动作,这个删除操作对应的函数被称之为删除器这个删除器函数本质是一个回调函数,我们只需要进行实现,其调用是由智能指针完成的。

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

class Test
{
public:
	Test() : m_num(0) { cout << "construct Test..." << endl; }
	Test(int x) : m_num(0) { cout << "construct Test, x = " << x << endl; }
	Test(string str) : m_num(0) { cout << "construct Test, str = " << str << endl; }
	~Test() { cout << "destruct Test..." << endl; }
	void setValue(int v) { this->m_num = v; }
	void print() { cout << "m_num: " << this->m_num << endl; }

private:
	int m_num;
};

int main()
{
	/*---------------------  一,初始化智能指针shared_ptr  ----------------------*/
	// 1.通过构造函数初始化
	shared_ptr<int> ptr1(new int(3));
	cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;

	// 2.通过移动和拷贝构造函数初始化
	shared_ptr<int> ptr2 = move(ptr1);
	cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;
	cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;

	shared_ptr<int> ptr3 = ptr2;
	cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;
	cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;

	// 3.通过 std::make_shared初始化
	shared_ptr<int> ptr4 = make_shared<int>(8);
	shared_ptr<Test> ptr5 = make_shared<Test>(7);
	shared_ptr<Test> ptr6 = make_shared<Test>("GOOD LUCKLY!");

	// 4.通过reset初始化
	ptr6.reset(); //重置ptr6, ptr6的引用基数为0
	cout << "ptr6管理的内存引用计数: " << ptr6.use_count() << endl;

	ptr5.reset(new Test("hello"));
	cout << "ptr5管理的内存引用计数: " << ptr5.use_count() << endl;

	cout << endl;
	cout << endl;

	/*----------------------  二,共享智能指针shared_ptr的使用  -------------------*/
	// 1.方法一
	Test *t = ptr5.get();
	t->setValue(1000);
	t->print();

	// 2.方法二
	ptr5->setValue(7777);
	ptr5->print();

	printf("\n\n");
	/*-----------------------------  三,指定删除器  ---------------------------*/
	// 1.简单举例
	shared_ptr<Test> ppp(new Test(100), [](Test *t)
						 {
        //释放内存
        cout << "Test对象的内存被释放了......." << endl;
        delete t; });
	printf("--------------------------------------------------------------------\n");

	2.如果是数组类型的地址,就需要自己写指定删除器,否则内存无法全部释放
	// shared_ptr<Test> p1(new Test[5], [](Test* t) {
	//     delete[]t;
	//     });

	// 3.也可以使用c++给我们提供的 默认删除器函数(函数模板)
	shared_ptr<Test>
		p2(new Test[3], default_delete<Test[]>());

	// 4.c++11以后可以这样写 也可以自动释放内存
	shared_ptr<Test[]> p3(new Test[3]);

	return 0;
}

10.2 std::unique_ptr

  1. 初始化

  std::unique_ptr是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针,可以通过它的构造函数初始化一个独占智能指针对象,但是不允许通过赋值将一个unique_ptr赋值给另一个unique_ptr

  1. 删除器

  unique_ptr指定删除器和shared_ptr指定删除器是有区别的,unique_ptr指定删除器的时候需要确定删除器的类型,所以不能像shared_ptr那样直接指定删除器

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

class Test
{
public:
	Test() : m_num(0) { cout << "construct Test..." << endl; }
	Test(int x) : m_num(1) { cout << "construct Test, x = " << x << endl; }
	Test(string str) : m_num(2) { cout << "construct Test, str = " << str << endl; }
	~Test() { cout << "destruct Test..." << endl; }
	void setValue(int v) { this->m_num = v; }
	void print() { cout << "m_num: " << this->m_num << endl; }

private:
	int m_num;
};

int main()
{
	/*--------------------------  一,初始化智能指针unique_ptr  ------------------------------*/
	// 1.通过构造函数初始化
	unique_ptr<int> ptr1(new int(3));

	// 2.通过移动函数初始化
	unique_ptr<int> ptr2 = move(ptr1);

	//.通过reset初始化
	ptr2.reset(new int(7));

	/*--------------------------  二,unique_ptr的使用  ------------------------------*/
	// 1.方法一
	unique_ptr<Test> ptr3(new Test(666));
	Test *pt = ptr3.get();
	pt->setValue(6);
	pt->print();

	// 2.方法二
	ptr3->setValue(777);
	ptr3->print();

	/*------------------------------------  三,指定删除器  -----------------------------------*/
	// 1.函数指针类型
	// using ptrFunc = void(*)(Test*);
	// unique_ptr<Test, ptrFunc> ptr4(new Test("hello"), [](Test* t) {
	//     cout << "-----------------------" << endl;
	//     delete t;
	//     });

	// 2.仿函数类型(利用可调用对象包装器)
	unique_ptr<Test, function<void(Test *)>> ptr4(new Test("hello"), [](Test *t){
        cout << "-----------------------" << endl;
        delete t; 
		});

	/*---------- 四,独占(共享)的智能指针可以管理数组类型的地址,能够自动释放 ---------*/
	unique_ptr<Test[]> ptr5(new Test[3]);

	//在c++11中shared_ptr不支持下面的写法,c++11以后才支持的
	shared_ptr<Test[]> ptr6(new Test[3]);

	return 0;
}

10.3 std::weak_ptr

  弱引用智能指针std::weak_ptr可以看做是shared_ptr的助手,它不管理shared_ptr内部的指针。std::weak_ptr没有重载操作符\*->,因为它不共享指针,不能操作资源,所以它的构造不会增加引用计数,析构也不会减少引用计数,它的主要作用就是作为一个旁观者监视shared_ptr中管理的资源是否存在。

  1. 通过调用std::weak_ptr类提供的**expired()方法来判断观测的资源是否已经被释放。**
  2. 通过调用std::weak_ptr类提供的**lock()方法来获取**管理所监测资源的shared_ptr对象。
  3. 通过调用std::weak_ptr类提供的**reset()方法来清空对象**,使其不监测任何资源。

😴十一、STL

STL主要是一些“容器”的集合,这些“容器”有list、vector、set、map,等等。
是世界上顶级C++程序员多年的杰作。是泛型编程的一个经典范例。

STL可分为六个部分:

  1. 容器 containers
  2. 迭代器 iterators
  3. 空间配置器 allocator
  4. 配接器 adapters
  5. 算法 algorithms
  6. 仿函数 functors

11.1 数组 vector

vector向量相当于一个数组

  在内存中分配一块连续的内存空间进行存储。支持不指定vector大小的存储。通常此默认的内存分配能完成大部分情况下的存储。

  • 优点:
    可以不指定大小,使用push_backpop_back来进行动态操作
    随机访问方便,即支持[ ]操作符和vector.at()
    节省空间
  • 缺点:
    在内部进行插入删除操作效率低
    只能在vector的最后进行pushpop,不能在vector的头进行pushpop
    当动态添加的数据超过vector默认分配的大小时要进行整体的重新分配、拷贝与释放

C++ vector的用法(整理) - young0173 - 博客园 (cnblogs.com)

11.2 栈 stack

特点:后进先出

c++ stack用法详解_斯文~的博客-CSDN博客_c++ stack

11.3 队列 queue

特点:先进先出

c++ queue 用法 - 清源居士 - 博客园 (cnblogs.com)

11.4 双向链表 list

每一个结点都包括一个信息块Info、一个前驱指针Pre、一个后驱指针Post。可以不分配必须的内存大小方便的进行添加和删除操作。使用的是非连续的内存空间进行存储。

  • 优点:
    不使用连续内存完成动态操作
    在内部方便的进行插入和删除操作
    可在两端进行pushpop
  • 缺点:
    不能进行内部的随机访问,即不支持[ ]操作符和vector.at()
    相对于verctor占用内存多

C++中list用法详解_Donny-You的博客-CSDN博客_c++ list

如果你需要高效的随即存取,而不在乎插入和删除的效率,使用vector
如果你需要大量的插入和删除,而不关心随机存取,则应使用list

11.5 映射 map

  map的元素是成对的键值/实值,内部的元素依据其值自动排序,map内的相同数值的元素只能出现一次,Multimap内可包含多个数值相同的元素,内部由二叉树实现(实际上基于红黑树(RB-tree)实现),便于查找

【c++】map用法详解_LeeMooq的博客-CSDN博客_c++ map


😶‍🌫️十二、标准转换函数

强制转换


#include <iostream>
using namespace std;

class Base
{
public:
    void test(void) { cout << "基类 test" << endl; }
};

class Derived : public Base
{
public:
    void test(void) { cout << "派生类 test" << endl; }
};

int main(void)
{
    Derived d1;
    Base *p_base = &d1;

    //如果是多态, p_base 会转换成Derived * 去访问到子类方法

    //强制转换,没有安全检查
    Derived *ptr = (Derived *)p_base;

    //采用dynamic_cast进行转换 (具有安全检查)
    Derived *ptr  = dynamic_cast <Derived *>(p_base);

    return 0;
}

转换函数 (本质 对类型 进行重载)

// 语法形式
operator 类型名( )   //转换函数
{
	实现转换的语句
}
explicit operator int( )   //用于防止隐式转换
{
	实现转换的语句
}

转换函数只能是成员函数,无返回值,空参数。
不能定义到void的转换,也不允许转换成数组或者函数类型。
转换常定义为const形式,原因是它并不改变数据成员的值。
慎用

//修饰 转换函数
//修饰 构造函数
用于防止隐式转换

C++提供了一些标准转换函数,建议使用标准转换函数进行类型转换
//分别有:reinterpret_castconst_caststatic_castdynamic_cast

  1. reinterpret_cast <new type> (expression)

    将一个类型的指针转换为另一个类型的指针,它也允许从一个指针转换为整数类型

  2. const_cast <new type> ( expression)

    const指针与普通指针间的相互转换,注意:不能将非常量指针变量转换为普通变量

  3. static_cast <new type>(expression)

    主要用于基本类型间的相互转换,和具有继承关系间的类型转换

  4. dynamic_cast <newtype>(expression)

    只有类中含有虚函数才能用dynamic_cast;仅能在继承类对象间转换
    dynamic_cast具有类型检查的功能,比static_cast更安全

#include <iostream>
using namespace std;

//类型转换
int main(void)
{
    int i;
    float f = 34.5;
    const char *ptr = "hello";

    //强制类型转换
    char *p = (char *)ptr;
    i = int(f);

    // C++建议使用标准转换函数
    p = const_cast<char *>(ptr); // const_cast 用于 const指针与普通指针间的相互转换

    char *p1;
    int *p2;
    p1 = reinterpret_cast<char *>(p2);

    /*
        reinterpret_cast
        reinterpret_cast<new type>(expression)
        将一个类型的指针转换为另一个类型的指针,它也允许从一个指针转换为整数类型
        const_cast
        const_cast<  new type>( expression)
        const指针与普通指针间的相互转换,注意:不能将非常量指针变量转换为普通变量
        static_cast
        static_cast<new type>(expression)
        主要用于基本类型间的相互转换,和具有继承关系间的类型转换
    */
    return 0;
}
#include <iostream>
using namespace std;

class A
{
public:
    explicit A(int n = 0)
    {
        cout << "A 构造:" << n << endl;
        this->i = n;
    }
    void set_val(int i)
    {
        this->i = i;
    }
    void print_val(void)
    {
        cout << i << endl;
    }
    explicit operator int()
    {
        return i;
    }
    // explicit 修饰转换方法  : 只能显式转换
private:
    int i;
};

int main(void)
{

#if 0
    A a1;
    a1.set_val(44);
    a1.print_val();

    A a2(34);
    cout<< int(a2)<<endl;  //显示转换
#endif

    A a1(43); //构造函数
    // A a2 = 74;  //构造构造 (隐式转换成  A a2(74) )

    int i = 32; // C风格的初始化
    int i(32);  // C++风格的初始化

#if 0
    cout<< a2+a1 <<endl;
    cout<< a2-a1 <<endl;
    cout<< a2*a1 <<endl;

    int ret = a2+5;
    cout<<ret<<endl;
    cout<<a2;

#endif

#if 0
    A a1(35);

    //将 A类型 转换 int 类型
    int i = int(a1); 
    cout<<i<<endl;
    

    //有了转换函数,编译器会很智能的进行 转换
    A a2(43);
    A a3 = a1+a2;
    cout<<a3<<endl;

#endif
    return 0;
}

注意:其实只有dynamic_cast,是具有安全检查的类型转换

文章目录

参考博客

  1. 参考博客:C++ 运算符总结_开箱剁手的博客-CSDN博客 ↩︎

  2. 参考博客:C++匿名对象_别忘了坚持的博客-CSDN博客_c++匿名对象 ↩︎

  3. 参考博客:C++友元函数、友元类、友元成员函数_芒种、的博客-CSDN博客 ↩︎

  4. 参考博客:十个 C++ 运算符重载示例,看完不懂打我…_阿基米东的博客-CSDN博客_c运算符重载实例 ↩︎

  5. 参考博客:C++ —— 友元函数_柚咖的博客-CSDN博客_c++友元函数 ↩︎

  6. 参考博客:C++虚函数详解_疯狂的麦克斯_max的博客-CSDN博客_c++虚函数 ↩︎

  7. 参考博客:C++中的错误处理方法(含示例代码)_虾米小馄饨的博客-CSDN博客_c++ 错误处理 ↩︎

  8. 参考博客:c++11之智能指针_峰上踏雪的博客-CSDN博客_智能指针 ↩︎

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值