一、基础
1.面向对象编程
C++ 完全支持面向对象的程序设计,包括面向对象开发的四大特性:
1)封装(Encapsulation)
封装是将数据和方法组合在一起,对外部隐藏实现细节,只公开对外提供的接口。这样可以提高安全性、可靠性和灵活性。
2)继承(Inheritance)
继承是从已有类中派生出新类,新类具有已有类的属性和方法,并且可以扩展或修改这些属性和方法。这样可以提高代码的复用性和可扩展性。
3)多态(Polymorphism)
多态是指同一种操作作用于不同的对象,可以有不同的解释和实现。它可以通过接口或继承实现,可以提高代码的灵活性和可读性。
4)抽象(Abstraction)
抽象是从具体的实例中提取共同的特征,形成抽象类或接口,以便于代码的复用和扩展。抽象类和接口可以让程序员专注于高层次的设计和业务逻辑,而不必关注底层的实现细节。
2.++a与a++
++a:先自增,再赋值
a++:先赋值,再自增
例:
int a = 3;
int b = a++;
cout << "a1 = " << a << ",b = "<<b<<endl; //a1 = 4,b = 3
int c = ++a;
cout << "a2 = " << a << ",c = "<<c<< endl; //a2 = 5,c = 5
3.格式输出
%d 整型输出
%.1f 保留一位小数且该小数进行四舍五入
4.关键字
C++总结(一)——关键字 - 知乎 (zhihu.com)
1)asm
含义
asm(指令字符串):在C++中嵌入汇编代码的时候需要用asm关键字
作用
汇编指令由字符串方式填在括号里,括号里能填一条或者多条汇编指令,有些编译器会把嵌入的汇编指令单独放在一个文件里编译。
函数内部的参数翻译为汇编指令,C语言环境下直接使用汇编指令执行。
nop
//作用:空等待汇编指令,仅仅起一个时间延时作用。
asm(“nop”);
//执行的是一条空指令(单周期指令)
#include <stdio.h>
nt main()
{
asm ("nop");
printf("hello");
asm ("nop nop "
"nop");
return 0;
}
2)explicit
C++ 的关键字(保留字)完整介绍 | 菜鸟教程 (runoob.com)
explicit(显式的)的作用是"禁止单参数构造函数"被用于自动型别转换,其中比较典型的例子就是容器类型。在这种类型的构造函数中你可以将初始长度作为参数传递给构造函数。
C++中的explicit详解_c++ explicit-CSDN博客
C++中的explicit关键字只能用于修饰只有一个参数的类构造函数, 它的作用是表明该构造函数是显示的, 而非隐式的, 跟它相对应的另一个关键字是implicit, 意思是隐藏的,类构造函数默认情况下即声明为implicit(隐式).
那么显示声明的构造函数和隐式声明的有什么区别呢? 我们来看下面的例子:
class CxString // 没有使用explicit关键字的类声明, 即默认为隐式声明
{
public:
char *_pstr;
int _size;
CxString(int size)
{
_size = size; // string的预设大小
_pstr = malloc(size + 1); // 分配string的内存
memset(_pstr, 0, size + 1);
}
CxString(const char *p)
{
int size = strlen(p);
_pstr = malloc(size + 1); // 分配string的内存
strcpy(_pstr, p); // 复制字符串
_size = strlen(_pstr);
}
// 析构函数这里不讨论, 省略...
};
// 下面是调用:
CxString string1(24); // 这样是OK的, 为CxString预分配24字节的大小的内存
CxString string2 = 10; // 这样是OK的, 为CxString预分配10字节的大小的内存
CxString string3; // 这样是不行的, 因为没有默认构造函数, 错误为: “CxString”: 没有合适的默认构造函数可用
CxString string4("aaaa"); // 这样是OK的
CxString string5 = "bbb"; // 这样也是OK的, 调用的是CxString(const char *p)
CxString string6 = 'c'; // 这样也是OK的, 其实调用的是CxString(int size), 且size等于'c'的ascii码
string1 = 2; // 这样也是OK的, 为CxString预分配2字节的大小的内存
string2 = 3; // 这样也是OK的, 为CxString预分配3字节的大小的内存
string3 = string1; // 这样也是OK的, 至少编译是没问题的, 但是如果析构函数里用
但是, 上面的代码中的_size代表的是字符串内存分配的大小, 那么调用的第二句 “CxString string2 = 10;” 和第六句 “CxString string6 = ‘c’;” 就显得不伦不类, 而且容易让人疑惑. 有什么办法阻止这种用法呢? 答案就是使用explicit关键字. 我们把上面的代码修改一下, 如下:
class CxString // 使用关键字explicit的类声明, 显示转换
{
public:
char *_pstr;
int _size;
explicit CxString(int size)
{
_size = size;
// 代码同上, 省略...
}
CxString(const char *p)
{
// 代码同上, 省略...
}
};
// 下面是调用:
CxString string1(24); // 这样是OK的
CxString string2 = 10; // 这样是不行的, 因为explicit关键字取消了隐式转换
CxString string3; // 这样是不行的, 因为没有默认构造函数
CxString string4("aaaa"); // 这样是OK的
CxString string5 = "bbb"; // 这样也是OK的, 调用的是CxString(const char *p)
CxString string6 = 'c'; // 这样是不行的, 其实调用的是CxString(int size), 且size等于'c'的ascii码, 但explicit关键字取消了隐式转换
string1 = 2; // 这样也是不行的, 因为取消了隐式转换
string2 = 3; // 这样也是不行的, 因为取消了隐式转换
string3 = string1; // 这样也是不行的, 因为取消了隐式转换, 除非类实现操作符"="的重载
explicit关键字只对有一个参数的类构造函数有效, 如果类构造函数参数大于或等于两个时, 是不会产生隐式转换的, 所以explicit关键字也就无效了。
但是, 也有一个例外, 就是当除了第一个参数以外的其他参数都有默认值的时候, explicit关键字依然有效, 此时, 当调用构造函数时只传入一个参数, 等效于只有一个参数的类构造函数。
3)export
C++ 的关键字(保留字)完整介绍 | 菜鸟教程 (runoob.com)
为了访问其他编译单元(如另一代码文件)中的变量或对象,对普通类型(包括基本数据类、结构和类),可以利用关键字 extern,来使用这些变量或对象时;但是对模板类型,则必须在定义这些模板类对象和模板函数时,使用标准 C++ 新增加的关键字 export(导出)。
4)extern
C++ 的关键字(保留字)完整介绍 | 菜鸟教程 (runoob.com)
extern(外部的)声明变量或函数为外部链接,即该变量或函数名在其它文件中可见。被其修饰的变量(外部变量)是静态分配空间的,即程序开始时分配,结束时释放。用其声明的变量或函数应该在别的文件或同一文件的其它地方定义(实现)。在文件内声明一个变量或函数默认为可被外部使用。在 C++ 中,还可用来指定使用另一语言进行链接,这时需要与特定的转换符一起使用。目前仅支持 C 转换标记,来支持 C 编译器链接。使用这种情况有两种形式:
extern "C" 声明语句
extern "C" { 声明语句块 }
5)friend
C++ 的关键字(保留字)完整介绍 | 菜鸟教程 (runoob.com)
friend(友元)声明友元关系。友元可以访问与其有 friend 关系的类中的 private/protected 成员,通过友元直接访问类中的 private/protected 成员的主要目的是提高效率。友元包括友元函数和友元类。
6)inline
C++ 的关键字(保留字)完整介绍 | 菜鸟教程 (runoob.com)
inline(内联)函数的定义将在编译时在调用处展开。inline 函数一般由短小的语句组成,可以提高程序效率。
7)namespace
C++ 的关键字(保留字)完整介绍 | 菜鸟教程 (runoob.com)
namespace(命名空间)用于在逻辑上组织类,是一种比类大的结构。
8)register
C++ 的关键字(保留字)完整介绍 | 菜鸟教程 (runoob.com)
register(寄存器)声明的变量称着寄存器变量,在可能的情况下会直接存放在机器的寄存器中;但对 32 位编译器不起作用,当 global optimizations(全局优化)开的时候,它会做出选择是否放在自己的寄存器中;不过其它与 register 关键字有关的其它符号都对32位编译器有效。
9)static
C++ 的关键字(保留字)完整介绍 | 菜鸟教程 (runoob.com)
static(静态的)静态变量作用范围在一个文件内,程序开始时分配空间,结束时释放空间,默认初始化为 0,使用时可改变其值。静态变量或静态函数,只有本文件内的代码才可访问它,它的名字(变量名或函数名)在其它文件中不可见。因此也称为"文件作用域"。在 C++ 类的成员变量被声明为 static(称为静态成员变量),意味着它被该类的所有实例所共享,也就是说当某个类的实例修改了该静态成员变量,其修改值为该类的其它所有实例所见;而类的静态成员函数也只能访问静态成员(变量或函数)。类的静态成员变量必须在声明它的文件范围内进行初始化才能使用,private 类型的也不例外。
10)volatile
C++ 的关键字(保留字)完整介绍 | 菜鸟教程 (runoob.com)
volatile(不稳定的)限定一个对象可被外部进程(操作系统、硬件或并发线程等)改变,声明时的语法如下:
int volatile nVint;
这样的声明是不能达到最高效的,因为它们的值随时会改变,系统在需要时会经常读写这个对象的值。因此常用于像中断处理程序之类的异步进程进行内存单元访问。
5.C++程序的基本框架
1).结构程序设计框架
在C++的结构化程序设计框架中,函数是程序的基本组成单元,;是程序中完成一定功能的模块。使用结构化程序设计方法编写出的C++程序包括一个主函数和若干个用户定义的函数。函数与函数之间是相对独立的并且是并行的,函数之间可以互相调用。同一个函数可以被一个或多个函数调用多次。主函数数由系统调用,在主函数中调用其他函数。
2).面向对象程序设计框架
面向对象的程序设计有三个主要的特征:封装、继承和多态。
(1)封装:是对象和类概念的主要特征。封装是指把方法和数居装起来,对数据的访问只能通过已定义的接口。封装使得对象的内部实现与外部接口分离开来,对象的内部实现的改变并不影响使用这个对象的其他对象或应用。
(2)继承:在面向对象的程序设计中,继承是指一个子类继承父类(或成为基类)的特征(数据结构和方法)。继承带来的好处是软件的复用,使用继承可以在已有软件的基础上构造新的软件,从而提高软件的生产率并保证软件的质量。
(3)多态:在面向对象程序设计中主要是指变量多态和方法多态;变量多态是指同一个变量在运行时刻标识(表示)不同类型的对象,而方法多态主要是指同一方法做不一样的动作。多态使得消息发送者能给一组具有公共接口的对象发送相同的消息,接受者做出相应的动作。变量多态是方法多态的基础嘿。
3)通常的框架包含以下部分(来源于AI)
-
包含必要的头文件。
-
定义命名空间,如
std
用于标准库。 -
主函数
main
,程序的入口点。 -
输出和输入,如cin和cout。
二、标准库-命名空间
1、std
调用某个模块里的功能
using namespace std //调用头文件<iostream>里的std命名空间里的功能
using std::cout; //与上面作用一致
using std::cin; //与上面作用一致
std::cout<<"hello"; //与上面作用一致
std::snprintf( char* buffer, std::size_t buf_size, const char* format, ... );
buffer是指向目标字符串地址的指针,format是以“%d”开头,以“\0”结尾的C风格字符串,定义数值以何种方式输出到目标字符串中
栗1:
char tmp[16];
int a = 100;
float b = 3.1415;
std::snprintf(tmp, sizeof(tmp), "gain[%d]=%.1f ",a,b ); //向tmp数组里,在tmp大小范围内,将a以整数方式输入到gain[],将b以保留一位小数输入到=后面
三、预处理
C++源程序中可以包含使用的各种编译命令,这些编译命令由于他们是在程序被正常编译之前执行的,故称为预处理命令。这些命令所实现的功能称为预处理功能。
所有的预处理命令在程序中都是以“#”来引导,每一条预处理命令单独占用一行,该行上不得再有预处理命令和语句。如果预处理命令一行写不下,可以续行,但有时需要添加续行符(\)。
预处理命令实际上是编译命令,他不是语句,一般不需要(;)结束。
预处理命令可放在程序开头、中间和末尾,主要由需要而定。
#define adc(name, ...) \
private: \
std::function<void(const xxx&)> name = [&](__xxx__)
1.文件包含命令(头文件)
用“xxx.h”引入自己写的头文件,用<xxx>引入系统头文件,引入头文件可以调用其中的功能。
#include "header.h"
#include <iostream>
2.条件编译命令
条件编译命令是定义一些条件,以使一些内容在这种特定的编译条件下进行编译。因此,利用条件编译可以使同一个源程序在不同的编译条件下产生不同的目标代码。
常见的条件编译命令有如下三种格式:
1)格式一
如果<标识符>被定义过,则内容1参与编译,如果没被定义过,则内容2参与编译.
内容1和内容2由若干条预处理命令和语句组成。
#ifdef <标识符>
//内容1
#else
//内容2
#endif
或
#ifdef <标识符>
//内容
#endif
2)格式二
如果<标识符>没被定义过,则内容1参与编译,如果被定义过,则内容2参与编译.
内容1和内容2由若干条预处理命令和语句组成。
#ifndef <标识符>
//内容1
#else
//内容2
#endif
或
#ifndef <标识符>
//内容
#endif
3)格式三
#if只有一个;#elif可以没有,也可以有多个;#else可以没有,也可以有一个
#if <常量表达式1>
//内容1
#elif <常量表达式2>
//内容2
#elif <常量表达式3>
//内容3
.........
#else
//内容n+1
#endif
4)格式四
#if 0
#if 0
code;
#endif
(1)code中定义的是一些调试版本的代码,此时code完全被编译器忽略。如果想让code生效,只需把#if 0改成#if 1
(2)#if 0还有一个重要的用途就是用来当成注释,如果你想要注释的程序很长,这个时候#if 0是最好的,保证不会犯错误。(但是林锐的书上说千万不要把#if 0 来当作块注释使用) #if 1可以让其间的变量成为局部变量。
(3)这个结构表示你先前写好的code,现在用不上了,又不想删除,就用这个方法,比注释方便。
3.宏定义命令
宏定义命令是用来将一个标识符定义为一个字符串,该标识符被称为宏名,被定义的字符串称为替换文本。
#define定义的常量,作用域是从定义开始,直到使用#undef取消其定义为止,如果不取消其定义,那么就直到整个文件结束为止。
标识符被宏定义后,在取消这次宏定义之前,不允许再对它宏定义。、
取消宏定义:#undef <标识符>
使用#define定义的标识符不是变量,因此不占用内存。
宏定义命令由两种格式:一种是简单的宏定义,一种是带参数的宏定义。
1)简单的宏定义
define是关键字,<宏名>是一个标识符,<字符串>是任意的字符序列。
#define <宏名><字符串>
例:
#define PI 3.14159265
#define M 10
一个标识符被宏定义后,该标识符便是一个宏名。这时,在程序出现的是宏名,在该程序被编译时,先将宏名用被定义的字符串替换,这称为宏替换,替换后才进行编译。宏替换是简单的代换。
例如:
#define M 10
int main()
{
int i = 5; int y;
y = 5*M; //被替换为:y = 5*10;之后再编译
cout<<y<<endl; //输出50
}
2)带参数的宏定义
#define <宏名> (<参数表>) <宏体>
<参数表>可以有一个参数,也可以有多个参数,使用“,”分隔。
例1:
#define ADD(x,y) x+y
int s = ADD(5,10); //被替换为 s = 5+10;
例2:
#include <iostream>
#define T 1
#define ABD void main() \
{cout << "hello," << s << endl;}
#include "abc.h"
abc.h:
#if T
char s[] = "good morning!";
ABC
#endif
被替换为:
#include <iostream>
char s[] = "good morning!";
void main() {cout << "hello," << s << endl;}
输出:hello,good morning!
四、指针
1.指针
指针是一个变量,其值为另一个变量的地址,即,内存位置的直接地址。就像其他变量或常量一样,您必须在使用指针存储其他变量地址之前,对其进行声明。指针变量声明的一般形式为
type_name* variable_name1 = &variable_name2;
如:
int a = 10;
int* p = &a; //p为指针变量,值为变量a的地址,*p = a = 10
注意:int* p与int *p没有实际差别,不过最好使用int* p格式,以便区分p与*p
2.指针常量和指向常量的指针(常量指针):(int* const 与 const int*)
1)指针常量:指针类型的常量,常量本身不能被更改
字符型常量:const char p = 'a'; //p不可以被更改
整数型常量:const int a = 0; //a不可以被更改
指针常量:int* const p = &a; //指针p不可以被更改,即地址不可以被更改
const修饰的指针,指针本身是个常量,该指针不可以再被赋予其他地址,但其指向的值可以发生改变。
如:
int a = 1;
int b = 2;
int* const p = &a; //const修饰指针变量p,指针本就是个地址,所以const修饰后,地址不可以被变更
*p = 5; //正确
p = &b; //编译出错,不可以再被赋予其他地址
2)常量指针(指向常量的指针): 指向常量的指针, 被指向的是常量,也就是值是常量,不可以被更改
整型指针:int* a; //指针指向整数
字符型指针: char* a; //指针指向字符
常量指针://指针指向常量
const修饰指针指向的实际值(即常量),该值(常量)不可以再被赋予其他值,但地址可以发生改变。
如:
int a = 1;
int b = 2;
const int* p = &a; //可以看作const修饰*p,*p为指向的值,所以值不可以被改变
*p = 5; //编译出错,其指向的值为常量,不可以再被赋予其他值
p = &b; //正确
3)
const int* const p = &a; //此时p不可以被改变地址,同时*p也不能改变值
3.空指针
为避免不必要的错误,指针需要指向确定的内存地址。如果没有确定的内存地址,则需要给指针赋予空值,即空指针。
例:
int* pa = 0;
int* pb = NULL;
int* pc = nullptr; //三者都是空指针
4.动态内存
1)动态内存
了解动态内存在 C++ 中是如何工作的是成为一名合格的 C++ 程序员必不可少的。C++ 程序中的内存分为两个部分:
- 栈:在函数内部声明的所有变量都将占用栈内存。
- 堆:这是程序中未使用的内存,在程序运行时可用于动态分配内存。
很多时候,您无法提前预知需要多少内存来存储某个定义变量中的特定信息,所需内存的大小需要在运行时才能确定。
在 C++ 中,您可以使用特殊的运算符为给定类型的变量在运行时分配堆内的内存,这会返回所分配的空间地址。这种运算符即 new 运算符。
如果您不再需要动态分配的内存空间,可以使用 delete 运算符,删除之前由 new 运算符分配的内存。
2)申请内存与释放内存
①通过new申请内存
int* point = new int; //开辟一块内存地址
*point = 100; //为新地址内的数据赋
如果自由存储区已被用完,可能无法成功分配内存。所以建议检查 new 运算符是否返回 NULL 指针,并采取以下适当的操作:
double* pvalue = NULL;
if( !(pvalue = new double ))
{
cout << "Error: out of memory." <<endl;
exit(1);
}
malloc() 函数在 C 语言中就出现了,在 C++ 中仍然存在,但建议尽量不要使用 malloc() 函数。new 与 malloc() 函数相比,其主要的优点是,new 不只是分配了内存,它还创建了对象。
②通过delete释放内存
自己申请的内存,一定要释放,否则该块内存会一直占用空间。而直接定义的变量则不用释放内存。
delete point; //释放上面自己开辟的内存
下面的实例中使用了上面的概念,演示了如何使用 new 和 delete 运算符:
#include <iostream>
using namespace std;
int main ()
{
double* pvalue = NULL; // 初始化为 null 的指针
pvalue = new double; // 为变量请求内存
*pvalue = 29494.99; // 在分配的地址存储值
cout << "Value of pvalue : " << *pvalue << endl;
delete pvalue; // 释放内存
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Value of pvalue : 29495
③通过new创建数组(同“数组的动态内存分配”)
int* p = new int[20]; //开辟一块新的内存空间,可以存放20个int类型数据
p[0] = 10; //为数组第一个元素赋值10
*(p+1) = 20; //为数组第二个元素赋值20
④通过delete释放数组内存(同“数组的动态内存分配”)
delete[] p; //释放刚刚开辟的数组内存
3)数组的动态内存分配
假设我们要为一个字符数组(一个有 20 个字符的字符串)分配内存,我们可以使用上面实例中的语法来为数组动态地分配内存,如下所示:
char* pvalue = NULL; // 初始化为 null 的指针
pvalue = new char[20]; // 为变量请求内存
要删除我们刚才创建的数组,语句如下:
delete [] pvalue; // 删除 pvalue 所指向的数组
下面是 new 操作符的通用语法,可以为多维数组分配内存,如下所示:
一维数组:
// 动态分配,数组长度为 m
int *array=new int [m];
//释放内存
delete [] array;
二维数组:
int **array;
// 假定数组第一维长度为 m, 第二维长度为 n
// 动态分配空间
array = new int *[m];
for( int i=0; i<m; i++ )
{
array[i] = new int [n];
}
//释放
for( int i=0; i<m; i++ )
{
delete [] array[i];
}
delete [] array;
二维数组实例测试:
#include <iostream>
using namespace std;
int main()
{
int **p;
int i,j; //p[4][8]
//开始分配4行8列的二维数据
p = new int *[4];
for(i=0;i<4;i++){
p[i]=new int [8];
}
for(i=0; i<4; i++){
for(j=0; j<8; j++){
p[i][j] = j*i;
}
}
//打印数据
for(i=0; i<4; i++){
for(j=0; j<8; j++)
{
if(j==0) cout<<endl;
cout<<p[i][j]<<"\t";
}
}
//开始释放申请的堆
for(i=0; i<4; i++){
delete [] p[i];
}
delete [] p;
return 0;
}
三维数组:
int ***array;
// 假定数组第一维为 m, 第二维为 n, 第三维为h
// 动态分配空间
array = new int **[m];
for( int i=0; i<m; i++ )
{
array[i] = new int *[n];
for( int j=0; j<n; j++ )
{
array[i][j] = new int [h];
}
}
//释放
for( int i=0; i<m; i++ )
{
for( int j=0; j<n; j++ )
{
delete[] array[i][j];
}
delete[] array[i];
}
delete[] array;
三维数组测试实例:
#include <iostream>
using namespace std;
int main()
{
int i,j,k; // p[2][3][4]
int ***p;
p = new int **[2];
for(i=0; i<2; i++)
{
p[i]=new int *[3];
for(j=0; j<3; j++)
p[i][j]=new int[4];
}
//输出 p[i][j][k] 三维数据
for(i=0; i<2; i++)
{
for(j=0; j<3; j++)
{
for(k=0;k<4;k++)
{
p[i][j][k]=i+j+k;
cout<<p[i][j][k]<<" ";
}
cout<<endl;
}
cout<<endl;
}
// 释放内存
for(i=0; i<2; i++)
{
for(j=0; j<3; j++)
{
delete [] p[i][j];
}
}
for(i=0; i<2; i++)
{
delete [] p[i];
}
delete [] p;
return 0;
}
4) 对象的动态内存分配
对象与简单的数据类型没有什么不同。例如,请看下面的代码,我们将使用一个对象数组来理清这一概念:
#include <iostream>
using namespace std;
class Box
{
public:
Box() {
cout << "调用构造函数!" <<endl;
}
~Box() {
cout << "调用析构函数!" <<endl;
}
};
int main( )
{
Box* myBoxArray = new Box[4];
delete [] myBoxArray; // 删除数组
return 0;
}
如果要为一个包含四个 Box 对象的数组分配内存,构造函数将被调用 4 次,同样地,当删除这些对象时,析构函数也将被调用相同的次数(4次)。
当上面的代码被编译和执行时,它会产生下列结果:
调用构造函数!
调用构造函数!
调用构造函数!
调用构造函数!
调用析构函数!
调用析构函数!
调用析构函数!
调用析构函数!
5.指针和数组的关系
1)数组本质是指针
数组本质是指针,可以用指针访问数组数据
int a[]{1,2,3,4,5,6};
cout << a << endl; //a为地址
cout << a+1 << endl; //a+1为a的下一个地址
cout << *a << endl; //*a指向数组的第一个数据,即 *a = 1
cout << *(a+1) << endl; //*(a+1)指向数组的第二个数据,即 *(a+1) = 2
*(a+1) = 10; //修改数组第二个数据的值
cout<< a[1] <<endl ; //a[1] = 10;
char s[] = "china"; //可以赋值,printf("s = %s",s),s = china
char* s = "china"; //s为指针,为地址,s = china
2)*(ptr)[i]与*ptr[i]
*(ptr)[i]:在一维数组里,指向第几个元素
*ptr[i]:在二维数组里,指向第几个一维数组(二维的元素)
int a[4][3] = {1,2,3,4,5,6,7,13,9,10,11,12}; //创建一个二维数组:一维数组包含3个元素,二维数组包含4个元素
int (*ptr)[3] = a; //指向一个含有三个元素的数组(指向了一维数组),指向a地址
cout << (*ptr)[3] <<endl; //指向a数组(一维数组)第4个元素,所以 (*ptr) [3] = 4
cout << *ptr[3] <<endl; //*ptr[3] = *(ptr[3]) = 10,即指向第4个一维数组里面的第1个元素,参考"1)"
cout << *ptr[0] <<endl; //*ptr[0] = 1
cout << *(ptr[2]+1) <<endl; //第3个一维数组里的第2个元素,结果为13
6.指针作为函数参数
指针作为函数实参传入函数时,传入的是变量的地址值,如果在函数改变传入的指针对应的变量值,那该变量的值会发生变化。实际上就是通过函数修改了某变量的值,与引用作为实参传入函数效果一致。
例:
#include <iostream>
using namespace std;
void func1(int* a);
void func2(int &a);
int main()
{
int i = 5;
cout << "i = " << i << endl; //i = 5
func1(&i); //传入变量地址,i的值已被改变,i = 10
cout << "func1---i = " << i << endl;
func2(i); //i的值再次发生改变,i = 15
cout << "func2---i = " << i << endl;
return 0;
}
void func1(int* a)
{
*a+=5;
cout << "func1---a = " << *a << endl;
}
void func2(int &a)
{
a+=5;
cout << "func2---a = " << a << endl;
}
输出结果:
i = 5 func1---a = 10 func1---i = 10 func2---a = 15 func2---i = 15
7.this指针
this是一个隐含于每一个类的成员函数中的特殊指针。该指针是一个指向正在被某个成员函数操作的对象的指针。
当对一个对象调用成员函数时,编译程序先将对象的地址赋值给this指针,然后调用成员函数,每次成员函数存取数据时,则隐含使用this指针。
例:
#include <iostream>
using namespace std;
class A
{
public:
A(){a = b = 0;}
A(int i,int j){a = i;b = j;}
void copy(A &a);
void print(){cout << "a = " << a << ", b = " << b << endl;}
private:
int a,b;
};
void A::copy(A &a)
{
//判断调用该函数的对象的地址与传入的实参对象的地址是否相同
//如果相同,则没必要继续执行;如果不同,将传入的实参对象的值赋值给调用该函数的对象
if(this == &a)
return;
*this = a;
}
int main()
{
A a1,a2(3,4);
a1.copy(a2); //a1地址赋值给this,a1的值赋值给*this
a1.print();
return 0;
}
8.字符串函数
参考嗨客网(C语言strcpy函数-C语言字符串拷贝-C语言 strcpy-嗨客网)
1)strcpy()
C语言中的 strcpy(string copy) 函数,用于对 字符串进行复制(拷贝)。strcpy 是一种 C 语言的标准库函数,strcpy 把含有 ‘\0’ 结束符的字符串复制到另一个地址空间,返回值的类型为 char*。strcpy()会替换目标字符串。
语法:
需要引用头文件:#include <string.h>
char* strcpy(char* strDestination, const char* strSource);
参数 | 描述 |
---|---|
strDestination | 目的字符串。 |
strSource | 源字符串。 |
栗1:
#include <stdio.h>
#include <string.h>
int main()
{
printf("嗨客网(www.haicoder.net)\n\n");
char dest[10] = { 0 };
char src[10] = "HaiCoder";
char *result = strcpy(dest, src);
printf("Dest = %s, Result = %s\n", dest, result); //Dest = HaiCoder, Result = HaiCoder
return 0;
}
栗2:
#include <stdio.h>
#include<string.h>
int main()
{
/* Write C code in this online editor and run it. */
//printf("Hello, World! \n");
char* p1;
char* p2;
char str[50]= "ABCDEFG";
p1= "abcd";
p2= "efg";
//printf("*(str+1) = %s,*(p2+1) = %s, *(str+3) = %s, *(p1+3) = %s",*(str+1),*(p2+1),*(str+3),*(p1+3));
char* res0 = strcpy(str,p2); //p2完全替换str, res1 = efg,str = efg
printf("Hello, World! \n res1 = %s,str = %s\n",res0,str);
char* res1 = strcpy(str+1,p2+1); //p2从第二位开始的元素,替换str从第二位开始的元素
printf("Hello, World! \n res1 = %s,str = %s\n",res1,str); // res1 = fg,str = efg
char* res2 = strcpy(str+3,p1+3);
printf("Hello, World! \n res1 = %s,res2 = %s,str = %s",res1,res2,str);
return 0;
}
输出结果:
Hello, World!
res1 = efg,str = efg
Hello, World!
res1 = fg,str = efg
Hello, World!
res1 = fgd,res2 = d,str = efgd
五、引用
1.引用
引用也是一种特殊类型的变量,它不同于指针。引用通常被认为是另一种变量的别名。
一般情况下,定义引用时必须初始化。
<类型>&<引用名>(<变量名>); 或 <类型>&<引用名> = <变量名>;
例
int a = 3;
int &m = a;
这里m是一个引用,他是变量a的别名。所有在引用上施加的操作,实质上就是在被引用者上的操作。
m = m+5;
实质上就是a+5,使a值改变为8.
可以将一个引用赋值给某个变量,则该变量具有被引用的变量的值
int * p = &m; //指向a的地址
2.函数的引用调用
引用主要是用来作函数的形参和函数的返回值。
使用引用作函数形参时,调用函数的实参要用变量名,将实参变量名赋给形参的引用,相当于在被调用函数中使用了实参的别名。于是在被调用函数中,对引用的改变,实质就是直接地通过引用来改变实参的变量值。
例:
#include <iostream>
using namespace std;
void change1(int x,int y);
void change2(int &x,int &y);
int main()
{
//cout << "Hello, world!" << endl;
int a = 1,b = 2;
change1(a,b); //a,b的值不会进行交换
cout << "a = " << a << ",b = " <<b<<endl;
change2(a,b); //a,b的值会进行交换
cout << "a = " << a << ",b = " <<b<<endl;
return 0;
}
void change1(int x,int y)
{
int temp = x;
x = y;
y = temp;
cout << "x = " << x << ",y = " <<y<<endl;
}
void change2(int &x,int &y) //引用调用
{
int temp = x;
x = y;
y = temp;
cout << "x = " << x << ",y = " <<y<<endl;
}
输出:
x = 2,y = 1 a = 1,b = 2 x = 2,y = 1 a = 2,b = 1
六、常类型
常类型是指使用类型修饰符const说明的类型,常类型的对象或变量的值不可以被更新。
1.一般常量
int const x = 2 或 const int x = 2,两者是一样的。此时,x不可以再被赋予其他值。
2.常对象
常对象是指对象常量,初始化后仍然不可以被更新。
const <类名> <对象名> 或 <类名> const <对象名>
例:
class A{ public: A(int i,int j){ x = i; y = j;}
private: int x,y; };
const A a1(3,4);
A const a2(5,6);
3.常指针
参见指针--常量指针与指向常量的指针
4.常引用
使用const修饰引用,被声明的引用为常引用。该常引用所引用的对象不能被更新。
const <类型说明符> &<引用名>
使用常指针、常引用等常量作为函数的形参,则表明该函数不会更新某个参数所指向或所引用的对象,这样在参数传递过程中就不需要执行拷贝初始化构造函数,这将改善程序的运行效率。
七、构造函数
1.构造函数
通过一个类创建一个对象,在初始化时,会调用构造函数。一个类可以有多个构造函数
格式:
1)类名::类名(params){函数体}
Student::Student(int age,int grade)
{
mAge = age;mGrade = grade;//mAge和mGrade是自己随便定义的变量。
}
2)类名::类名(params){函数体}:初始化列表
不能被赋值的变量:const变量、引用、IO流对象
如果类中包含这三类成员,一定要使用初始化列表对他们进行初始化工作
Student::Student(int age,int grade):mAge(age),mGrade(grade)
{
}
调用时:Student s1(15,3);
2.默认构造函数
不带任何初值(参数),或者参数赋予默认值,即为默认构造函数。一个类只能有一个默认构造函数。
Student student; //默认构造函数
Student student(int age = 0,int grade = 0); Student student; //默认构造函数,也不需要传入任何参数
3.拷贝构造函数
拷贝构造函数是一个特殊的构造函数,其形参为本类的对象引用。使用一个已经存在的对象去初始化同类的一个新对象,就会调用拷贝构造函数。
格式:
类名(形参);//构造函数
类名(const 类名 &参数名);//拷贝构造函数
类名::类名(const 类名 &参数名){函数体} //拷贝构造函数的定义
例1:
Class Point
{
Public:
Point(const Point &p):x(p.x),y(p.y){cout<<"..."} //拷贝构造函数的定义,当用一个已经存在的对象去初始化同类的一个新对象,就会调用拷贝构造函数。
Private:
int x,y;
}
void main(void)
{
Point A(1,2);
Point B(A); //用已存在的对象去初始化一个同类的对象,此时拷贝构造函数被调用
Point B = A; //同上
}
例2:若函数的形参为类对象,调用函数时,实参赋给形参,形参自动调用拷贝构造函数。
void foo(Point p) //
{
cout<<p.GetX()<<endl;
}
void main()
{
Point A(1,2);
foo(A); //函数参数的传递,实际就是拷贝,实参拷贝给形参。此时调用拷贝构造函数
}
例3:当函数的返回值是类对象时,系统自动调用拷贝构造函数。
Point foo2()
{
Point A(1,2);
return A; //调用拷贝构造函数
}
void main()
{
Point B;
B = foo2(); //发生拷贝行为
}
4.浅拷贝与深拷贝
1)浅拷贝:两个指针同时指向同一块内存地址。
2)深拷贝:两个指针指向两个内容相同的内存地址。
八、析构函数
用来完成对象被删除前的一些清理工作。析构函数是在对象的生命周期即将结束时被自动调用的。
格式:
类名::~类名(){函数体}
注:
1.函数名为~类名
2.函数没有参数,没有返回值
3.函数体负责资源清理工作
需要析构函数的时机:
析构函数通常用于释放在构造函数或在对象生命周期内获取的资源(通过构造函数申请资源时,需要析构函数销毁资源)
例:
class Array
{
public:
Array(int size):nSize(size){pArray = new int[nSize];}
~Array(){delete[] pArray;pArray = NULL;} //析构函数
};
九、面向对象
一)成员函数
1.类的成员函数
在类的定义中规定在类体中说明的函数作为类的成员,称为成员函数。特殊的成员函数包括:构造函数、析构函数、拷贝初始化构造函数等。
类成员函数是类的一个成员,它可以操作类的任意对象,可以访问对象中的所有成员。
现在我们要使用成员函数来访问类的成员,而不是直接访问这些类的成员:
class Box
{
public:
double length; // 长度
double breadth; // 宽度
double height; // 高度
double getVolume(void);// 返回体积
};
成员函数可以定义在类定义内部,或者单独使用范围解析运算符 :: 来定义。在类定义中定义的成员函数把函数声明为内联的,即便没有使用 inline 标识符(ps:这句话似乎是说:在类中定义成员函数,该成员函数为内联函数,即使没有用Inline标识符)。所以您可以按照如下方式定义 getVolume() 函数:
class Box
{
public:
double length; // 长度
double breadth; // 宽度
double height; // 高度
double getVolume(void)
{
return length * breadth * height;
}
};
您也可以在类的外部使用范围解析运算符 :: 定义该函数,如下所示:
double Box::getVolume(void)
{
return length * breadth * height;
}
在这里,需要强调一点,在 :: 运算符之前必须使用类名。调用成员函数是在对象上使用点运算符(.),这样它就能操作与该对象相关的数据,如下所示:
Box myBox; // 创建一个对象
myBox.getVolume(); // 调用该对象的成员函数
让我们使用上面提到的概念来设置和获取类中不同的成员的值:
#include <iostream>
using namespace std;
class Box
{
public:
double length; // 长度
double breadth; // 宽度
double height; // 高度
// 成员函数声明
double getVolume(void);
void setLength( double len );
void setBreadth( double bre );
void setHeight( double hei );
};
// 成员函数定义
double Box::getVolume(void)
{
return length * breadth * height;
}
void Box::setLength( double len )
{
length = len;
}
void Box::setBreadth( double bre )
{
breadth = bre;
}
void Box::setHeight( double hei )
{
height = hei;
}
// 程序的主函数
int main( )
{
Box Box1; // 声明 Box1,类型为 Box
Box Box2; // 声明 Box2,类型为 Box
double volume = 0.0; // 用于存储体积
// box 1 详述
Box1.setLength(6.0);
Box1.setBreadth(7.0);
Box1.setHeight(5.0);
// box 2 详述
Box2.setLength(12.0);
Box2.setBreadth(13.0);
Box2.setHeight(10.0);
// box 1 的体积
volume = Box1.getVolume();
cout << "Box1 的体积:" << volume <<endl;
// box 2 的体积
volume = Box2.getVolume();
cout << "Box2 的体积:" << volume <<endl;
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Box1 的体积: 210
Box2 的体积: 1560
2.内联函数
内联函数是指那些定义在类体内的成员函数,即该函数的函数体放在类体内。
1).内联函数引入的原因
在调用普通的函数时,代码执行顺序转到代码中进行,执行完后再转回原代码为止继续执行,此举势必增加了时间和空间的消耗,降低运行效率。
在程序编译时,编译器会将出现调用内敛函数的地方与内联函数体进行替换,所以不会出现转来转去的情况,但会增加目标程序的代码量,增加了空间开销。
2).内联函数定义方法
在普通函数前加 inline
例
inline int add(int a,int b)
{
return a+b;
}
int main()
{
int x = 1,y = 2;
int z = add(1,2);
//编译时,此处被替换为 return x+y;
}
3).注意事项
1)在内联函数中,不允许使用循环语句和开关语句
2)内联函数的定义必须出现在内联函数第一次被调用之前
3.外联函数
外联函数是指说明在类体内,定义在类体外的成员函数。
4.类构造函数 & 析构函数
C++ 类构造函数 & 析构函数 | 菜鸟教程 (runoob.com)
1).类的构造函数
类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。
构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。
下面的实例有助于更好地理解构造函数的概念:
#include <iostream>
using namespace std;
class Line
{
public:
void setLength( double len );
double getLength( void );
Line(); // 这是构造函数
private:
double length;
};
// 成员函数定义,包括构造函数
Line::Line(void)
{
cout << "Object is being created" << endl;
}
void Line::setLength( double len )
{
length = len;
}
double Line::getLength( void )
{
return length;
}
// 程序的主函数
int main( )
{
Line line;
// 设置长度
line.setLength(6.0);
cout << "Length of line : " << line.getLength() <<endl;
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Object is being created
Length of line : 6
2).带参数的构造函数
默认的构造函数没有任何参数,但如果需要,构造函数也可以带有参数。这样在创建对象时就会给对象赋初始值,如下面的例子所示:
#include <iostream>
using namespace std;
class Line
{
public:
void setLength( double len );
double getLength( void );
Line(double len); // 这是构造函数
private:
double length;
};
// 成员函数定义,包括构造函数
Line::Line( double len)
{
cout << "Object is being created, length = " << len << endl;
length = len;
}
void Line::setLength( double len )
{
length = len;
}
double Line::getLength( void )
{
return length;
}
// 程序的主函数
int main( )
{
Line line(10.0);
// 获取默认设置的长度
cout << "Length of line : " << line.getLength() <<endl;
// 再次设置长度
line.setLength(6.0);
cout << "Length of line : " << line.getLength() <<endl;
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Object is being created, length = 10
Length of line : 10
Length of line : 6
3).使用初始化列表来初始化字段
使用初始化列表来初始化字段:
Line::Line( double len): length(len)
{
cout << "Object is being created, length = " << len << endl;
}
上面的语法等同于如下语法:
Line::Line( double len)
{
length = len;
cout << "Object is being created, length = " << len << endl;
}
假设有一个类 C,具有多个字段 X、Y、Z 等需要进行初始化,同理地,您可以使用上面的语法,只需要在不同的字段使用逗号进行分隔,如下所示:
C::C( double a, double b, double c): X(a), Y(b), Z(c)
{
....
}
4).类的析构函数
类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。
析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。
下面的实例有助于更好地理解析构函数的概念:
#include <iostream>
using namespace std;
class Line
{
public:
void setLength( double len );
double getLength( void );
Line(); // 这是构造函数声明
~Line(); // 这是析构函数声明
private:
double length;
};
// 成员函数定义,包括构造函数
Line::Line(void)
{
cout << "Object is being created" << endl;
}
Line::~Line(void)
{
cout << "Object is being deleted" << endl;
}
void Line::setLength( double len )
{
length = len;
}
double Line::getLength( void )
{
return length;
}
// 程序的主函数
int main( )
{
Line line;
// 设置长度
line.setLength(6.0);
cout << "Length of line : " << line.getLength() <<endl;
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Object is being created
Length of line : 6
Object is being deleted
二)继承
1.概念
面向对象程序设计中最重要的一个概念是继承。继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行效率的效果。
当创建一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类,新建的类称为派生类。
继承代表了 is a 关系。例如,哺乳动物是动物,狗是哺乳动物,因此,狗是动物,等等。
// 基类
class Animal {
// eat() 函数
// sleep() 函数
};
//派生类
class Dog : public Animal {
// bark() 函数
};
2.基类 & 派生类
一个类可以派生自多个类,这意味着,它可以从多个基类继承数据和函数。定义一个派生类,我们使用一个类派生列表来指定基类。类派生列表以一个或多个基类命名,形式如下:
class derived-class: access-specifier base-class
其中,访问修饰符 access-specifier 是 public、protected 或 private 其中的一个,base-class 是之前定义过的某个类的名称。如果未使用访问修饰符 access-specifier,则默认为 private。
假设有一个基类 Shape,Rectangle 是它的派生类,如下所示:
#include <iostream>
using namespace std;
// 基类
class Shape
{
public:
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};
// 派生类
class Rectangle: public Shape
{
public:
int getArea()
{
return (width * height);
}
};
int main(void)
{
Rectangle Rect;
Rect.setWidth(5);
Rect.setHeight(7);
// 输出对象的面积
cout << "Total area: " << Rect.getArea() << endl;
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Total area: 35
3.访问控制和继承
派生类可以访问基类中所有的非私有成员。因此基类成员如果不想被派生类的成员函数访问,则应在基类中声明为 private。
我们可以根据访问权限总结出不同的访问类型,如下所示:
访问 | public | protected | private |
---|---|---|---|
同一个类 | yes | yes | yes |
派生类 | yes | yes | no |
外部的类 | yes | no | no |
一个派生类继承了所有的基类方法,但下列情况除外:
- 基类的构造函数、析构函数和拷贝构造函数。
- 基类的重载运算符。
- 基类的友元函数。
4.继承类型
当一个类派生自基类,该基类可以被继承为 public、protected 或 private 几种类型。继承类型是通过上面讲解的访问修饰符 access-specifier 来指定的。
我们几乎不使用 protected 或 private 继承,通常使用 public 继承。当使用不同类型的继承时,遵循以下几个规则:
- 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。
- 保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。
- 私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。
5.多继承
多继承即一个子类可以有多个父类,它继承了多个父类的特性。
C++ 类可以从多个类继承成员,语法如下:
class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,…
{
<派生类类体>
};
其中,访问修饰符继承方式是 public、protected 或 private 其中的一个,用来修饰每个基类,各个基类之间用逗号分隔,如上所示。现在让我们一起看看下面的实例:
#include <iostream>
using namespace std;
// 基类 Shape
class Shape
{
public:
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};
// 基类 PaintCost
class PaintCost
{
public:
int getCost(int area)
{
return area * 70;
}
};
// 派生类
class Rectangle: public Shape, public PaintCost
{
public:
int getArea()
{
return (width * height);
}
};
int main(void)
{
Rectangle Rect;
int area;
Rect.setWidth(5);
Rect.setHeight(7);
area = Rect.getArea();
// 输出对象的面积
cout << "Total area: " << Rect.getArea() << endl;
// 输出总花费
cout << "Total paint cost: $" << Rect.getCost(area) << endl;
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Total area: 35
Total paint cost: $2450
三)多态
1.概念
多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。
C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。
下面的实例中,基类 Shape 被派生为两个类,如下所示:
#include <iostream>
using namespace std;
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
int area()
{
cout << "Parent class area :" <<endl;
return 0;
}
};
class Rectangle: public Shape{
public:
Rectangle( int a=0, int b=0):Shape(a, b) { }
int area ()
{
cout << "Rectangle class area :" <<endl;
return (width * height);
}
};
class Triangle: public Shape{
public:
Triangle( int a=0, int b=0):Shape(a, b) { }
int area ()
{
cout << "Triangle class area :" <<endl;
return (width * height / 2);
}
};
// 程序的主函数
int main( )
{
Shape *shape;
Rectangle rec(10,7);
Triangle tri(10,5);
// 存储矩形的地址
shape = &rec;
// 调用矩形的求面积函数 area
shape->area();
// 存储三角形的地址
shape = &tri;
// 调用三角形的求面积函数 area
shape->area();
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Parent class area :
Parent class area :
导致错误输出的原因是,调用函数 area() 被编译器设置为基类中的版本,这就是所谓的静态多态,或静态链接 - 函数调用在程序执行前就准备好了。有时候这也被称为早绑定,因为 area() 函数在程序编译期间就已经设置好了。
但现在,让我们对程序稍作修改,在 Shape 类中,area() 的声明前放置关键字 virtual,如下所示:
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
virtual int area()
{
cout << "Parent class area :" <<endl;
return 0;
}
};
修改后,当编译和执行前面的实例代码时,它会产生以下结果:
Rectangle class area :
Triangle class area :
此时,编译器看的是指针的内容,而不是它的类型。因此,由于 tri 和 rec 类的对象的地址存储在 *shape 中,所以会调用各自的 area() 函数。
正如您所看到的,每个子类都有一个函数 area() 的独立实现。这就是多态的一般使用方式。有了多态,您可以有多个不同的类,都带有同一个名称但具有不同实现的函数,函数的参数甚至可以是相同的。
2.虚函数
虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。
我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。
(可以参考上面的(面向对象--多态--概念))
3.纯虚函数
您可能想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是您在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。
我们可以把基类中的虚函数 area() 改写如下:
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
// pure virtual function
virtual int area() = 0;
};
= 0 告诉编译器,函数没有主体,上面的虚函数是纯虚函数。
4.接口(抽象类)
C++ 接口(抽象类) | 菜鸟教程 (runoob.com)
1)概念
接口描述了类的行为和功能,而不需要完成类的特定实现。
C++ 接口是使用抽象类来实现的,抽象类与数据抽象互不混淆,数据抽象是一个把实现细节与相关的数据分离开的概念。
如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。纯虚函数是通过在声明中使用 "= 0" 来指定的,如下所示:
class Box
{
public:
// 纯虚函数
virtual double getVolume() = 0;
private:
double length; // 长度
double breadth; // 宽度
double height; // 高度
};
设计抽象类(通常称为 ABC)的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。如果试图实例化一个抽象类的对象,会导致编译错误。
因此,如果一个 ABC 的子类需要被实例化,则必须实现每个纯虚函数,这也意味着 C++ 支持使用 ABC 声明接口。如果没有在派生类中重写纯虚函数,就尝试实例化该类的对象,会导致编译错误。
可用于实例化对象的类被称为具体类。
2)抽象类的实例
请看下面的实例,基类 Shape 提供了一个接口 getArea(),在两个派生类 Rectangle 和 Triangle 中分别实现了 getArea():
#include <iostream>
using namespace std;
// 基类
class Shape
{
public:
// 提供接口框架的纯虚函数
virtual int getArea() = 0;
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};
// 派生类
class Rectangle: public Shape
{
public:
int getArea()
{
return (width * height);
}
};
class Triangle: public Shape
{
public:
int getArea()
{
return (width * height)/2;
}
};
int main(void)
{
Rectangle Rect;
Triangle Tri;
Rect.setWidth(5);
Rect.setHeight(7);
// 输出对象的面积
cout << "Total Rectangle area: " << Rect.getArea() << endl;
Tri.setWidth(5);
Tri.setHeight(7);
// 输出对象的面积
cout << "Total Triangle area: " << Tri.getArea() << endl;
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Total Rectangle area: 35
Total Triangle area: 17
从上面的实例中,我们可以看到一个抽象类是如何定义一个接口 getArea(),两个派生类是如何通过不同的计算面积的算法来实现这个相同的函数。
3)设计策略
面向对象的系统可能会使用一个抽象基类为所有的外部应用程序提供一个适当的、通用的、标准化的接口。然后,派生类通过继承抽象基类,就把所有类似的操作都继承下来。
外部应用程序提供的功能(即公有函数)在抽象基类中是以纯虚函数的形式存在的。这些纯虚函数在相应的派生类中被实现。
这个架构也使得新的应用程序可以很容易地被添加到系统中,即使是在系统被定义之后依然可以如此。
十、函数的递归调用
1.概念
所谓函数的递归调用是指在调用一个函数的过程中,出现直接地或间接地调用函数自身。将原有的问题分解为一个新的问题,而新的问题又用到了原有问题的解法,这就出现了递归。
在实际问题中,有许多问题可以采取递归的调用方法来解决。
2.递归调用的过程
1)第一阶段-“递推”
将原问题不断分解为新的子问题,逐渐从未知的向已知的方向推测,最终达到已知的条件,即递归结束条件,这时递推阶段结束。
2)第二阶段-“回归”
该阶段是从已知的条件出发,按照“递推”的逆过程,逐一求值回归,最后达到递推的开始处,结束回归阶段,完成递归调用。
例1:求3!的结果
//递推阶段
3! = 3 x 2!
2! = 2 x 1!
1! = 1 x 0!
0! = 1
//回归阶段
0! = 1
1! = 1 x 1
2! = 2 x 1
3! = 3 x 2 = 6
#include <iostream>
using namespace std;
long int function(int n);
int main()
{
//cout << "Hello, world!" << endl;
int a = 3;
cout << "a! = " << function(a);
return 0;
}
long int function(int n)
{
int p;
if(n == 0)
{
p = 1;
}
else
{
p = n * function(n-1);
}
return p;
}
十一、静态成员
静态成员的提出是为了解决数据共享的问题。全局变量可以解决共享问题,但会破坏安全性,可能在某处不小心被更新。
全局静态成员只在声明它的文件中可见。
static变量和普通变量的区别:
static全局变量与普通全局变量区别:static全局变量只初使化一次,防止在其他文件单元中被引用;
static局部变量和普通局部变量区别:static局部变量只被初始化一次,下一次依据上一次结果值;
static函数与普通函数区别:static函数在内存中只有一份,普通函数在每个被调用中维持一份拷贝。
1.静态数据成员
静态数据成员是类的所有对象中共享的成员,而不是某个对象的成员。
使用静态成员可以节省内存,因为他是对所有对象共有的。
定义静态数据成员的格式:static int x;
初始化静态数据成员的格式:<数据类型><类名>::<静态数据成员名> = <值>;
这表明:
(1)初始化在类体外进行,而前面不加static,以免与一般静态变量或对象相混淆。
(2)初始化时不加该成员的访问控制权限控制符private、public等。
(3)初始化时使用作用域运算符来标明它所属的类,因此静态数据成员是类的成员,而不是对象的成员。
例:
#include <iostream>
using namespace std;
class Myclass
{
public:
Myclass(int a,int b,int c);
void GetNumber();
void GetSum();
private:
int A,B,C;
static int Sum; //静态变量的声明
};
int Myclass::Sum = 0; //静态变量的定义(初始化)
Myclass::Myclass(int a,int b,int c)
{
A = a;
B = b;
C = c;
Sum += A+B+C; //静态变量的运算
}
void Myclass::GetNumber()
{
cout << "Number = " << A << "," << B << "," << C <<endl;
}
void Myclass::GetSum()
{
cout<< "Sum = " << Sum << endl;
}
int main()
{
Myclass M(3,7,10),N(14,9,11);
M.GetNumber(); //静态变量更新:Sum = 20
N.GetNumber(); //静态变量更新:Sum = 54
M.GetSum();
N.GetSum();
return 0;
}
输出结果为:
Number = 3,7,10 Number = 14,9,11 Sum = 54 Sum = 54
2.静态成员函数
静态成员函数同样是类的静态成员,而不是对象成员。
在静态成员函数的实现中不能直接引用类中声明的非静态成员,可以引用类中声明的静态成员。如果静态成员函数要引用非静态成员时,可通过对象来引用。
例:
#include <iostream>
using namespace std;
class M
{
public:
M(int a){A = a;B += a;} //构造函数
static void f1(M m); //静态成员函数声明
private:
int A;
static int B; //静态变量
};
int M::B = 0;
void M::f1(M m) //静态成员函数定义(实现)
{
cout << "A = " << m.A << endl; //通过类的对象引用类的非静态成员
cout << "B = " << B << endl; //直接引用类的静态成员
}
int main()
{
M P(5); //B = 5
M Q(10); //B = 15
M::f1(P); //静态函数的调用
M::f1(Q);
return 0;
}
输出结果为:
A = 5 B = 15 A = 10 B = 15
十二、友元
1.友元函数
友元函数是能够访问类中的私有成员的非成员函数。友元函数从语法上看,与普通函数一样,即在定义上和调用上与普通函数一样。
格式:在函数声明前添加friend关键字
例:
#include <iostream>
#include <math.h>
using namespace std;
class Point
{
public:
Point(double xx,double yy){x = xx;y = yy;} //构造函数
void Getxy();
friend double Distance(Point &a,Point &b); //声明友元函数
private:
double x,y;
};
void Point::Getxy()
{
cout << "(" << x << "," << y << ")" << endl;
}
double Distance(Point &a,Point &b)
{
//定义友元函数时,不用添加类名的限制
double dx = a.x - b.x; //访问了Point类的私有成员
double dy = a.y - b.y;
return sqrt(dx * dx + dy * dy);
}
int main()
{
Point p1(3.0,4.0),p2(6.0,8.0);
p1.Getxy();
p2.Getxy();
double d = Distance(p1,p2); //友元函数的调用,直接被调用,访问了对象p1,p2的私有成员
cout << "Distance is " << d << endl;
return 0;
}
输出结果为:
(3,4) (6,8) Distance is 5
2.友元类
一个类可以作为另一个类的友元。当一个类作为另一个类的友元时,这个类的所有成员函数都将成为另一个类的友元函数。
例:
#include <iostream>
#include <math.h>
using namespace std;
class X
{
friend class Y; //声明Y类为X的友元类
public:
void Set(int i){x = i;}
void Display(){cout << "x = " << x << ",";
cout << "y = " << y << endl;}
private:
int x;
static int y;
};
class Y
{
public:
Y(int i,int j);
void Display();
private:
X a;
};
int X::y = 1;
Y::Y(int i,int j)
{
a.x = i; //因为Y是X的友元类,所以可以访问X类中的私有变量x
X::y = j; //静态变量共享
}
void Y::Display()
{
cout << "x = " << a.x << ","; //因为Y是X的友元类,所以可以访问X类中的私有变量x
cout << "y = " << X::y << endl;//静态变量共享
}
int main()
{
X b;
b.Set(5);
b.Display();
Y c(6,9);
c.Display();
b.Display();
return 0;
}
说明:因为Y是X的友元类,所以可以访问X类的私有成员x和私有静态变量y,普通的类无法做到这一点。
输出结果:
x = 5,y = 1 x = 6,y = 9 x = 5,y = 9
十三、结构体
struct定义变量,便于重复定义
struct Position
{ float x;
float y;
float z; };
int main()
{
Position player = {1,2.2,3};
Position enemy {3,3,3}; //两种定义方式都可以
cout << player.x<<endl;
Position enemies[] = {(1,1,1),(2,2,2)}; //Position 类型的数组
cout<<enemies[1].x<<endl; //2,数组内第二个敌人的x坐标
}
十四、枚举
enum定义固定的数值,以便后期做判断等行为。
enum HeroType
{
Tank, //默认为0,
ADC = 2,//可以自己赋初值
Magic //在上一个值的基础上加1,此处Magic = 3
};
int main()
{
HeroType heroType = ADC; //enum定义
}
十五、数组
1.普通创建数组
int a[]{1,2,3,4,5,6};
int a[] = {1,2,3,4,5,6};
int a[6] = {1,2,3,4,5,6};
2.通过c++11模板类array创建数组
#include <array>
array<int,3> a1 = {6,2,7}; //3代表数组大小。当数组内的数据不满3个时,剩下的数据为0
array<int,3> a2;
a2 = a1; //可以通过此方式直接用一个数组给另一个数组赋值
3.二维数组
二维数组可以看作元素为一维数组的一维数组
例
int i[2][3] = {(0,1,2),(3,4,5)};
其中,(0,1,2)和(3,4,5)都作为元素
4.三维数组
三维数组可以看作元素为一维数组的二维数组,二维数组又可以看作元素为一维数组的一维数组。
例1:
int i[2][2][3] = {(0,1,2),(3,4,5),(6,7,8),(9,10,11)};
此三维数组可以看作为两个二维数组,其元素为((0,1,2),(3,4,5))和((6,7,8),(9,10,11))。
例2:
static char t[][3][5] = {"abcd","efgh","ijkl","mnop","qrst","uvwx"};
cout<<t[1][2][3]<<endl; //二维素组元素个数为3,所以三个一维数组组成一个二维数组,
//那么t[1][2][3]访问的就是("mnop","qrst","uvwx")中的"uvwx"中的x
cout << *(t[1][2]+3) << *(*(*(t+1)+2)+3) << *(*(t[1]+2)+3) << endl;//与上述结果相同
十六、vector 容器
C++ vector 容器 | 菜鸟教程 (runoob.com)
C++ 中的 vector 是一种序列容器,它允许你在运行时动态地插入和删除元素。
vector 是基于数组的数据结构,但它可以自动管理内存,这意味着你不需要手动分配和释放内存。
与 C++ 数组相比,vector 具有更多的灵活性和功能,使其成为 C++ 中常用的数据结构之一。
vector 是 C++ 标准模板库(STL)的一部分,提供了灵活的接口和高效的操作。
1.基本特性
1)动态大小
vector
的大小可以根据需要自动增长和缩小。
2)连续存储
vector
中的元素在内存中是连续存储的,这使得访问元素非常快速。
3)可迭代
vector
可以被迭代,你可以使用循环(如 for
循环)来访问它的元素。
4)元素类型
vector
可以存储任何类型的元素,包括内置类型、对象、指针等。
2.使用场景
- 当你需要一个可以动态增长和缩小的数组时。
- 当你需要频繁地在序列的末尾添加或移除元素时。
- 当你需要一个可以高效随机访问元素的容器时。
要使用 vector,首先需要包含 <vector> 头文件:
#include <vector>
3.创建 Vector
创建一个 vector 可以像创建其他变量一样简单:
std::vector<int> myVector; // 创建一个存储整数的空 vector
这将创建一个空的整数向量,也可以在创建时指定初始大小和初始值:
std::vector<int> myVector(5); // 创建一个包含 5 个整数的 vector,每个值都为默认值(0)
std::vector<int> myVector(5, 10); // 创建一个包含 5 个整数的 vector,每个值都为 10
或:
std::vector<int> vec; // 默认初始化一个空的 vector
std::vector<int> vec2 = {1, 2, 3, 4}; // 初始化一个包含元素的 vector
4. 添加元素
可以使用 push_back 方法向 vector 中添加元素:
myVector.push_back(7); // 将整数 7 添加到 vector 的末尾
5.访问元素
可以使用下标操作符 [] 或 at() 方法访问 vector 中的元素:
int x = myVector[0]; // 获取第一个元素
int y = myVector.at(1); // 获取第二个元素
6.获取大小
可以使用 size() 方法获取 vector 中元素的数量:
int size = myVector.size(); // 获取 vector 中的元素数量
7. 迭代访问
可以使用迭代器遍历 vector 中的元素:
for (auto it = myVector.begin(); it != myVector.end(); ++it) {
std::cout << *it << " ";
}
或者使用范围循环:
for (int element : myVector) {
std::cout << element << " ";
}
8.删除元素
可以使用 erase() 方法删除 vector 中的元素:
myVector.erase(myVector.begin() + 2); // 删除第三个元素
9.清空 Vector
可以使用 clear() 方法清空 vector 中的所有元素:
myVector.clear(); // 清空 vector
10.实例
以下是一个完整的使用实例,包括创建 vector、添加元素、访问元素以及输出结果的代码:
#include <iostream>
#include <vector>
int main() {
// 创建一个空的整数向量
std::vector<int> myVector;
// 添加元素到向量中
myVector.push_back(3);
myVector.push_back(7);
myVector.push_back(11);
myVector.push_back(5);
// 访问向量中的元素并输出
std::cout << "Elements in the vector: ";
for (int element : myVector) {
std::cout << element << " ";
}
std::cout << std::endl;
// 访问向量中的第一个元素并输出
std::cout << "First element: " << myVector[0] << std::endl;
// 访问向量中的第二个元素并输出
std::cout << "Second element: " << myVector.at(1) << std::endl;
// 获取向量的大小并输出
std::cout << "Size of the vector: " << myVector.size() << std::endl;
// 删除向量中的第三个元素
myVector.erase(myVector.begin() + 2);
// 输出删除元素后的向量
std::cout << "Elements in the vector after erasing: ";
for (int element : myVector) {
std::cout << element << " ";
}
std::cout << std::endl;
// 清空向量并输出
myVector.clear();
std::cout << "Size of the vector after clearing: " << myVector.size() << std::endl;
return 0;
}
以上代码创建了一个整数向量,向其中添加了几个元素,然后输出了向量的内容、元素的访问、向量的大小等信息,接着删除了向量中的第三个元素,并输出删除元素后的向量。最后清空了向量,并输出清空后的向量大小。
输出结果为:
Elements in the vector: 3 7 11 5
First element: 3
Second element: 7
Size of the vector: 4
Elements in the vector after erasing: 3 7 5
Size of the vector after clearing: 0
十七、线程
多线程是多任务处理的一种特殊形式,多任务处理允许让电脑同时运行两个或两个以上的程序。一般情况下,两种类型的多任务处理:基于进程和基于线程。
- 基于进程的多任务处理是程序的并发执行。
- 基于线程的多任务处理是同一程序的片段的并发执行。
1.创建线程
(假设使用的是 Linux 操作系统,要使用 POSIX 编写多线程 C++ 程序)
引入头文件 pthread.h
#include<pthread.h>
pthread_t t_a;
pthread_t t_b;
pthread_create(&t_a,attr,start_routine,arg);
pthread_create(&t_b,attr,start_routine,arg);
参数thread是一个指针(即地址),使用时的格式:pthread(&xx,....);
编辑 编辑
2.终止线程
(终止POSIX线程)
在这里,pthread_exit 用于显式地退出一个线程。通常情况下,pthread_exit() 函数是在线程完成工作后无需继续存在时被调用。
如果 main() 是在它所创建的线程之前结束,并通过 pthread_exit() 退出,那么其他线程将继续执行。否则,它们将在 main() 结束时自动被终止。
#include<pthread.h>
pthread_exit(status);
3.互斥锁上锁与解锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; -----初始化互斥锁
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//init cond
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr) ---动态创建条件变量
pthread_mutex_lock ---互斥锁上锁
pthread_mutex_unlock ----互斥锁解锁
pthread_cond_wait() / pthread_cond_timedwait -----等待条件变量,挂起线程,区别是后者会有timeout时间,如 果到了timeout,线程自动解除阻塞,这个时间和 time()系统调用相同意义的。以1970年时间算起。
pthread_cond_signal ----激活等待列表中的线程,
pthread_cond_broadcast() -------激活所有等待线程列表中最先入队的线程
注意:1)上面这几个函数都是原子操作,可以为理解为一条指令,不会被其他程序打断
2)上面这个几个函数,必须配合使用。
3)pthread_cond_wait,先会解除当前线程的互斥锁,然后挂线线程,等待条件变量满足条件。一旦条件变量满足条件,则会给线程上锁,继续执行pthread_cond_wait
十八、崩溃
关键字 backtrace
backtrace如果有BuildID,需要先查看本地对应编译的so的BuildID:
readelf -n xxxx.so //c++编译生成的库文件,此命令可以查看到buildID
查看so库指定地址代码:
addr2line -p -C -f -i -e ./so/xxxxx.so 0000000000068fcc //地址符,可以查看到代码第几行
十九、模板
1.概念
模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码。
模板是创建泛型类或函数的蓝图或公式。库容器,比如迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。
每个容器都有一个单一的定义,比如 向量,我们可以定义许多不同类型的向量,比如 vector <int> 或 vector <string>。
您可以使用模板来定义函数和类,接下来让我们一起来看看如何使用。
2.函数模板
模板函数定义的一般形式如下所示:
template <typename type> ret-type func-name(parameter list)
{
// 函数的主体
}
在这里,type 是函数所使用的数据类型的占位符名称。这个名称可以在函数定义中使用。
下面是函数模板的实例,返回两个数中的最大值:
#include <iostream>
#include <string>
using namespace std;
template <typename T>
inline T const& Max (T const& a, T const& b)
{
return a < b ? b:a;
}
int main ()
{
int i = 39;
int j = 20;
cout << "Max(i, j): " << Max(i, j) << endl;
double f1 = 13.5;
double f2 = 20.7;
cout << "Max(f1, f2): " << Max(f1, f2) << endl;
string s1 = "Hello";
string s2 = "World";
cout << "Max(s1, s2): " << Max(s1, s2) << endl;
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Max(i, j): 39
Max(f1, f2): 20.7
Max(s1, s2): World
3.类模板
正如我们定义函数模板一样,我们也可以定义类模板。泛型类声明的一般形式如下所示:
template <class type> class class-name {
.
.
.
}
在这里,type 是占位符类型名称,可以在类被实例化的时候进行指定。您可以使用一个逗号分隔的列表来定义多个泛型数据类型。
下面的实例定义了类 Stack<>,并实现了泛型方法来对元素进行入栈出栈操作:
#include <iostream>
#include <vector>
#include <cstdlib>
#include <string>
#include <stdexcept>
using namespace std;
template <class T>
class Stack {
private:
vector<T> elems; // 元素
public:
void push(T const&); // 入栈
void pop(); // 出栈
T top() const; // 返回栈顶元素
bool empty() const{ // 如果为空则返回真。
return elems.empty();
}
};
template <class T>
void Stack<T>::push (T const& elem)
{
// 追加传入元素的副本
elems.push_back(elem);
}
template <class T>
void Stack<T>::pop ()
{
if (elems.empty()) {
throw out_of_range("Stack<>::pop(): empty stack");
}
// 删除最后一个元素
elems.pop_back();
}
template <class T>
T Stack<T>::top () const
{
if (elems.empty()) {
throw out_of_range("Stack<>::top(): empty stack");
}
// 返回最后一个元素的副本
return elems.back();
}
int main()
{
try {
Stack<int> intStack; // int 类型的栈
Stack<string> stringStack; // string 类型的栈
// 操作 int 类型的栈
intStack.push(7);
cout << intStack.top() <<endl;
// 操作 string 类型的栈
stringStack.push("hello");
cout << stringStack.top() << std::endl;
stringStack.pop();
stringStack.pop();
}
catch (exception const& ex) {
cerr << "Exception: " << ex.what() <<endl;
return -1;
}
}
当上面的代码被编译和执行时,它会产生下列结果:
7
hello
Exception: Stack<>::pop(): empty stack