第1章:杂叙
1.名字空间 namespace xxx 在main函数中划分变量 空间时,需要 指定“xxx::a=1”
2.倘若需要使用cin 和 cout,需要在main函数外使用 using std::cin;或者从 using std::cout;标准库中的名字都属于标准名字空间std
3.变量存在的意义时为了方便管理内存空间
4 .程序块内定义内部变量,作用域在程序块内,隐藏外部变量。
6.引用变量 double &b=a;//b是a的别名,b就是a,引用变量经常用作函数的 形式参数,表示形参和实参是同一个对象,修改实参就修改形参。
7.swap函数交换x,y,内部需要借助临时变量temp去接收x,y中的x。 当交换数据内存较大时,最好不要复制数值,可以使用指针去交换地址。
8.define 定义宏常量,const修饰的变量,都是不可修改(写入函数体内部),否则报错。
9.
-
重写 是在派生类中改变基类方法的行为。
-
重载 是在同一个类中为同一个操作提供多种方法实现。
10.单冒号 ":" 是指子类继承基类
双冒号 “::”指出函数是属于哪个类的 " BOOL MyAPP::InitInstance()
",指出 InitInstance
函数是 MyAPP
类的成员,用于域解析。
11.std命名空间:
-
基础类型:
-
std::size_t
-
std::ptrdiff_t
-
-
字符串操作:
-
std::string
-
std::wstring
-
std::string_view
(C++17)
-
-
输入输出:
-
std::cout
-
std::cin
-
std::cerr
-
std::endl
-
std::flush
-
-
数学库:
-
std::abs
-
std::sqrt
-
std::pow
-
std::sin
,std::cos
,std::tan
等
-
-
数据结构:
-
std::vector
-
std::list
-
std::deque
-
std::array
-
std::map
-
std::unordered_map
-
std::set
-
std::unordered_set
-
std::stack
-
std::queue
-
std::priority_queue
-
-
算法:
-
std::sort
-
std::find
-
std::count
-
std::copy
-
std::transform
-
std::accumulate
-
-
内存管理:
-
std::malloc
-
std::calloc
-
std::realloc
-
std::free
-
-
异常处理:
-
std::exception
-
std::bad_alloc
-
std::runtime_error
-
-
时间日期:
-
std::chrono
-
-
输入输出流操作:
-
std::ios
-
std::iostream
-
std::stringstream
-
-
文件系统访问 (C++17):
-
std::filesystem
-
-
多线程和同步 (C++11):
-
std::thread
-
std::mutex
-
std::lock_guard
-
std::condition_variable
-
-
智能指针 (C++11):
-
std::unique_ptr
-
std::shared_ptr
-
std::weak_ptr
-
-
原子操作和线程安全 (C++11):
-
std::atomic
-
std::atomic_flag
-
-
正则表达式 (C++11):
-
std::regex
-
std::regex_search
-
-
元编程:
-
std::enable_if
-
std::conditional
-
-
类型特征 (C++11):
-
std::is_same
-
std::is_integral
-
std::is_floating_point
-
-
函数特性 (C++11):
-
std::function
-
std::bind
-
-
随机数生成 (C++11):
-
std::mt19937
-
std::uniform_int_distribution
-
-
容器访问:
-
std::begin
-
std::end
-
std::rbegin
-
std::rend
-
第2章:数据类型
语法: 数据类型 变量名 = 变量初始值
int a = 10; 数据类型存在的意义:为了给变量分配合适的内存空间。(合适内存空间,不会造成内存浪费)
数据类型 | 占用空间 |
---|---|
short | 2字节 |
int | 4字节 |
long | win为4字节,linux为4字节 |
longlong | 8字节 |
2.1sizeof关键字
作用:利用sizeof可以获得数据类型占用的内存空间大小,代码如下:
short num1=10;
cout<<"short 占用的内存空间大小"<<sizeof(num1)<<"字节"<<endl;
2.2实型(浮点型)
作用:表示小数,包含单精度浮点数float 和 双精度浮点数double
数据类型 | 占用空间 | 有效数字范围 |
---|---|---|
float | 4字节 | 7位有效数字 |
double | 8字节 | 15—16位有效数字 |
下面示例
int main(){ //1.单精度float //2.双精度double float f1 = 3.14f; cout<<"f1="<<f1<<endl; double d1 = 3.14; cout<<"d1="<<d1<<endl; system("pause"); return 0; }
2.3字符型变量
#include <iostream> using namespace std ; int main(){ //字符型变量创建方式 char ch=a; //字符型变量大小sizeof cout<<"字符变量所占内存空间大小:"<<sizeof(char)<<endl; system("pause"); return 0; }
大写字母”A“的ASCII值是65,小写字母的”a“的ASCII值是97
类型 | 关键字 |
---|---|
整型 | [signed] int |
无符号整型 | unsigned int |
有符号短整型 | [signed] short (int) |
无符号短整型 | unsigned short (int) |
有符号长整型 | [signed] long (int) |
无符号长整型 | unsigned long [int] |
注意事项!
不能串接使用使用关系运算符:a<b<c
在C++中,表示连续小于时,a<b&&a<c
操作符 | 功能 | 目数 | 用法 |
---|---|---|---|
& | 位逻辑“与” | 双目 | expr1 &expr2 |
| | 位逻辑“或” | 双目 | expr1 | expr2 |
^ | 位逻辑“异或” | 双目 | expr1 ^ expr2 |
~ | 位逻辑“取反” | 单目 | ~expr |
2.4条件运算符
2.4.1三目运算符
形式: <表达式1> ? <表达式2> : <表达式3> ,意思为“若表达式1为真,则执行表达式2;若为假,则执行表达式3”
2.4.2逗号运算符
逗号“,”,也是源自中运算符,称为逗号运算符,优先等级的最低,自左向右,共嗯是将两个表达式连接城一个表达式。
形式为:<表达式1>,<表达式2>,<表达式3>....<表达式n>
2.4.3类型转换
规则:若参与运算的类型不同,则先转换成同一类型,然后进行运算。赋值时,一般赋值符号右变量的类型转换成左变量的类型。转换按数据由低到高顺序执行,确保数据精度不降低。
2.5条件判断语句
特别注意 if--else if结构:
if(表达式1) 语句1; else if(表达式2) 语句2; ... else if (表达式m) 语句m; else 语句n;
switch - case 结构
switch (表达式){ case常量表达式1://判断,成立,执行语句1;不成立,跳转表达式2 语句1;//上述常量表达式1成立,在这执行语句1 break; case常量表达式2://判断,成立,执行语句2;不成立,跳转表达式3 语句2;//上述常量表达式2成立,在这执行语句2 break; ... case常量表达式n: 语句n; break; default: 语句n+1; break; }
第3章:函数
函数参数与返回值:表达式类型必须与函数的返回值类型相同,或者是能够隐式转换成函数有的返回值类型。
int add(int x, int y){ return(x+y); }
如果返回值是 void 类型的函数,想要在程序执行某个位置是可以推出,可以通过return 语句实现。
void OutPut(int x){ if (x<0) return ; else cout<<"x的值是:"<< x << endl; }
不返回函数值的函数,可以明确定义为“空类型”,标识符为“void”
void ShowMessage(){ cout<<"这是一个" }
3.1形参与实参
错误表达方式:
int GetMax(int x, int y = 10,int z) { /*codes*/ }
正确表达方式:
int GetMax(int x, int y,int z = 10) { /*codes*/ } //或者下面一种方式 int GetMax(int x, int y = 10, int z = 10) { /*codes*/ }
总结:如果某一形参有默认值,那么其后面的所有参数都需要有默认值!
3.2函数调用
/*包含头文件*/ int MAX(int x, int y);//开头声明函数 int MAX(int x , int y )//定义函数 { return x>y?x:y; } void main() //调用函数 { int larger = MAX(2,3); }
3.3变量作用域
在程序中,局部变量和全局变量的名称可以相同,但是在函数内,局部变量的值会覆盖全局变量的值。
//示例 #include <iostream> using namespace std; // 全局变量声明 int g = 20; int main () { // 局部变量声明 int g = 10; cout << g; return 0; }
此时输出结果为:10
3.4 函数重载(重写)
允许在同一作用域中的某个**函数和运算符指定多个定义,分别称为函数重载和运算符重载。
函数重载
在同一个作用域内,可以声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同。
#include <iostream> using namespace std; class printData //声明一个类PrintData来封装重载函数,虽然是封装 ,但是预留一个public接口允许外界访问 { public: // 设定访问级别,指定类成员是公开的 void print(int i) { cout << "整数为: " << i << endl; } void print(double f) { cout << "浮点数为: " << f << endl; } void print(char c[]) { cout << "字符串为: " << c << endl; } }; int main(void) { printData pd; // 输出整数 pd.print(5); // 输出浮点数 pd.print(500.263); // 输出字符串 char c[] = "Hello C++"; pd.print(c); return 0; }
函数重写
-
定义:函数重写发生在继承体系中,派生类提供了与基类中具有相同名称、相同参数列表的函数。
-
目的:允许派生类改变或扩展基类中虚函数的行为。
-
基类中的虚函数:为了使函数可以被重写,基类中的函数需要声明为虚函数(使用
virtual
关键字)。 -
运行时解析:运行时多态,编译器在运行时根据对象的实际类型来确定调用哪个函数。
函数重载与函数重写的区别:
-
作用域:重载发生在同一个类内,而重写发生在基类和派生类之间。
-
参数匹配:重载要求参数列表不同,而重写要求参数列表与基类中的虚函数完全相同。
-
虚函数:重写通常与虚函数一起使用,而重载不涉及虚函数的概念。
-
多态性:重载是编译时多态,重写是运行时多态。
-
访问控制:在重写时,派生类的函数访问级别不应低于基类中被重写的函数。
class Shape { public: virtual void draw() { std::cout << "Drawing a shape" << std::endl; } }; class Circle : public Shape { public: void draw() override { std::cout << "Drawing a circle" << std::endl; } };
3.5运算符重载
允许程序员为自定义类型定义运算符的行为。运算符重载通过函数重载实现
运算符重载函数的语法如下:
[ReturnType] operator[operator](参数列表...);
汽车轮子的运算符重载的例子
class Car { //声明一个Car类 public: int wheels; // 轮子数量 // 构造函数 Car(int w) : wheels(w) {} // 重载加法运算符,非成员函数形式 friend Car operator+(const Car& a, const Car& b) { return Car(a.wheels + b.wheels); } }; Car car1(4); // 创建一个有4个轮子的车 Car car2(3); // 创建一个有3个轮子的车 Car car3 = car1 + car2; // 使用重载的加法运算符,car3 将有 7 个轮子
3.6内联函数
通过inline关键字可以把函数定义为内联函数,通常在头文件下方进行定义
声明和定义如下:
inline 返回类型 函数名(参数列表) { // 函数体 }
示例
inline int max(int a, int b) { return a > b ? a : b; } int main() { int x = 5, y = 10; int result = max(x, y); // 这里 max 函数可能会被内联展开 return 0; }
解释内联函数在此段代码的作用:利用 inline关键字声明一个 max 函数,在
main函数中调用内联函数max,编译器将max函数代码直接展开到调用点。
3.7虚函数与实函数
虚函数(Virtual Functions):
-
多态性:虚函数是实现多态性的关键。它们允许派生类重写(Override)基类的函数,使得同一个函数调用根据对象的实际类型而表现出不同的行为。
-
基类指针或引用:虚函数通常通过基类的指针或引用被调用,这样可以根据指向的对象的实际类型来确定调用哪个函数。
-
虚函数表:C++通过虚函数表(VTable)机制来支持虚函数。每个有虚函数的类都有一个虚函数表,存储了虚函数的地址。
-
动态绑定:虚函数支持动态绑定(Dynamic Binding),即在运行时确定调用哪个函数。
-
声明方式:在基类中使用
virtual
关键字声明函数,然后在派生类中重写它。 -
纯虚函数:如果虚函数没有实现,它可以被声明为纯虚函数(使用
= 0
),这样的类成为抽象类,不能被实例化。
class Base { public: virtual void show() { std::cout << "Base show" << std::endl; } };
实函数(Non-Virtual Functions,或称为 Concrete Functions):
-
单一行为:实函数在编译时绑定,总是调用其所属类定义的函数,不考虑对象的实际类型。
-
直接调用:实函数可以直接通过对象、指针或引用调用。
-
没有虚函数表:实函数不使用虚函数表,因此调用效率可能略高于虚函数。
-
覆盖:如果派生类中有一个与基类同名的实函数,它会隐藏(而不是重写)基类中的同名虚函数。
-
声明方式:不需要使用
virtual
关键字,这是默认的行为。
class Base { public: void display() { std::cout << "Base display" << std::endl; } };
使用场景:
-
虚函数:当你需要在派生类中改变函数的行为,或者当你使用多态性时,应该使用虚函数。
-
实函数:当你希望函数在所有类中表现相同的行为,或者当你不关心对象的动态类型时,应该使用实函数。
第4章:变量
变量的存储类别:auto,static,register,extern
4.1 auto变量
{ int i,j,k; }
{ auto int i,j,k; }
特点:1.自动变量的作用域仅限于定义该变量的个体内“{}”
2.自动变量属于动态存储方式
3.不同个体允许使用同名的变量而不会因混淆造成报错
4.2 static变量
static int a, int b ;
特点:1.静态变量在函数内定义,在程序退出时释放
2.静态变量的作用域与自动变量相同,在哪个{}内定义,就在哪生效
3.编译器给静态变量赋值0
4.3 register变量
寄存器变量属于动态存储方式。凡是需要静态存储方式的变量就不能定义为寄存器变量
第5章:数组与字符串
有序数据的集合称为数组。一个数组有一个统一的数组名,可以通过数组名和下标来唯一确定数组的元素。
5.1一维数组
声明形式: 数据类型 数组名[常量表达式]
示例:
int a[10]; //声明一个整型数组,下标从0-9,包含10个元素 char name[128]; //声明一个字符数组,下标从0-127,包含128个元素 float price[20]; //声明一个浮点数组,下标从0-19,包含20个元素
数组说明:1.数组名的定义规则和变量名相同
2.定义数组的常量表达式不能是变量这就意味着数组大小必须要在 编译时确定
3.数组可以动态内存分配或者使用标准库容器在运行是动态定义
//动态内存分配数组内存 ----new int n = 10; // 运行时确定的大小 int* arr = new int[n]; // 使用数组... delete[] arr; // 记得释放内存
//使用std::vector: //std::vector 是C++标准模板库(STL)中的一种容器,它可以在运行时调整 大小。 #include <vector> std::vector<int> vec; int n = 10; // 运行时确定的大小 vec.resize(n); // 使用 vector...
5.2二维数组
声明形式: 数据类型 数组名[常量表达式1] [常量表达式2]
举例
float MyArray[4][5] //声明具有4行5列的元素的浮点数组
同样,定义数组的常量表达式不能是变量
int a[i][j]; //i,j都需要声明为常量,如 int i=3,j=4
对于二维数组赋值存在的两种方法,一个是单一数组赋值,另一个是聚合方法赋值。
//单一数组元素赋值 MyArray[0][1]=12;
//聚合方法赋值 int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12} int b[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}}
赋值顺序是根据行列顺序进行一一赋值
5.3字符数组
字符数组是用来存放字符数据,字符数组的一个元素存放一个字符。
同样,对字符数组也具有两种赋值方式,一个是单一数组元素赋值,另一个是聚合方法赋值。
注意:聚合赋值方式只能在数组声明的时候使用,如下!
char pWord[5]={'H','E','L','L',O} ; //此种表达方式是正确的 char pWord[5]; pWord = {'H','E','L','L','O'} //此种方法是错误的,上面一行 定义数组,下面聚合赋值没有在定义char类型字符数组时使用
同时,字符数组不能给字符数组赋值,意思可理解为一个已赋值过的字符数组不能直接给未赋值的数组进行赋值操作。如下:
char a[5]={'H','E','L','L',O}; char b[5]; a=b; //这种赋值操作是不行的,字符数组间不能直接进行赋值操作 a[0]=b[0]; //这种赋值是可以接受的
字符数组常常用来作字符串使用,作为字符要有字符串结束符“\0".
虽然上述的字符数组间不能互相进行赋值操作,但是字符串可以给字符数组赋值。例子如下:
char a[]="HELLO WORLD\0";
5.4字符串处理函数
5.4.1stract函数
-
字符串连接函数stract格式:
stract(字符数组1,字符数组2)
-
字符串连接函数stract功能
将字符数组2的字符串连接到字符数组1中的字符串的后面,并删去字符串1后的结束标志“\0”
#include <iostream> #include <string> using namespace std; void main() { char str1[30],str2[10]; //定义两个字符数组str1, str2,将str2拼接到str1后 cout<<"Please input str1:"<<endl; gets(str1); //调用gets函数,从键盘获得字符串 cout<<"Please input str2:"<<endl; gets(str2); //调用gets函数,从键盘获得字符串 stract(str1,str2);//调用stract函数进行字符串拼接 //此时拼接结束,str1已经是拼接后 cout<<"The new str1 is :" puts("str1"); //puts函数输出 return 0 ; }
特别注意:当使用stract函数对字符串进行拼接,一定要确保被拼接的字符数组足够大,如若数组空位不够去接收外来字符串(字符数组剩余空位不足),则会导致数据损坏或者缓冲区溢出。
若要处理位置字符拼接问题,可以采取两种方式:1.使用安全函数strncat等函数,允许程序员指定最大复制的字符数。2.动态内存管理,在拼接后重新分配足够的内存空间。
如下时strncat函数
char dest[100]; // 假设已经有一个以null结尾的字符串 const char* src = "Hello, World!"; size_t src_len = strlen(src); strncat(dest, src, sizeof(dest) - strlen(dest) - 1); // 确保有足够的空间并保留null字符位置
5.4.2strcpy函数
-
字符串赋值函数strcpy格式如下:
strcpy(字符数组1,字符数组2);
-
字符串复制函数功能:
将字符数组2复制到字符数组1中,利用结束标识符“\0”
注意事项:确保字符数组1的长度足够长,若字符数组2长度比字 符数组1的有限长度空间长,则复制失败。
#include <iostream> #include <cstring> // 包含字符串库,用于strcpy等函数 using namespace std; void main() { const char* str = "Hello, World!"; // 源字符串 char destStr[50]; // 目标字符数组,足够大以存储源字符串的副本 // 使用strcpy复制字符串 strcpy(destStr, str); // 将str的内容复制到destStr,此时destStr已经复制完成 // 输出复制后的字符串 cout << "Copied string is: " << destStr << endl; }
5.4.3strcmp函数
-
字符串比较函数strcmp格式:
strcmp(字符数组1,字符数组2);
-
字符串复制函数strcmp功能:
1.按照ASCII值进行比较
2.字符串1=字符串2,则返回值为0
3.字符串1>字符串2,则返回值为一个正数
4.字符串1<字符串2,则返回值为一个负数
#include <iostream> #include <cstring> // 包含strcmp函数的头文件 using namespace std; int main() { const char* str1 = "Hello"; const char* str2 = "World"; int result; // 使用strcmp比较两个字符串 result = strcmp(str1, str2); // 根据strcmp的返回值输出比较结果 if (result == 0) { cout << "The strings are equal." << endl; } else if (result < 0) { cout << "The first string is less than the second string." << endl; } else { cout << "The first string is greater than the second string." << endl; } return 0 ; }
5.4.4strlen函数
-
字符串比较函数strlen格式如下:
strlen(字符数组1,字符数组2);
例子如下:
#include <iostream> #include <cstring> // 包含strlen函数的头文件 using namespace std; void main() { const char* myString = "Hello, World!"; size_t length; // 使用strlen计算字符串的长度 length = strlen(myString); // 输出字符串的长度 cout << "The length of the string is: " << length << endl; }
上述4个函数是对字符串的处理函数,值得注意的是,在c++中,通常推荐std: : string类来处理字符串,在其内部提供丰富的成员函数。
例如,若要测得字符串长度,可以调用std内部的成员函数。
#include <iostream> #include <string> using namespace std; void main() { string myString = "Hello, World!";//定义一个string类型的变量 myString,并将字符串 Hello, World!赋值给myString size_t length; //size_t是一个数据类型,用于表示大小或者长度,通常以字节为长度 // 使用std::string的length成员函数 length = myString.length(); // 输出字符串的长度 cout << "The length of the string is: " << length << endl; }
第6章:指针与数组
-
内存地址:指针存储了变量在内存中的地址。
-
指针变量:声明指针时,需要指定它所指向的变量的类型。例如,
int* p;
声明了一个指向int
类型的指针(整型指针)。 -
引用操作:使用解引用操作符
*
可以访问指针指向的变量的值。例如,*p
获取了p
指针所指向的int
变量的值。 -
地址运算符:
&
运算符用于获取变量的地址,这个地址可以被赋给一个指针。 -
指针的指针:可以声明指向指针的指针,如
int** pp;
,这在实现某些数据结构(例如二叉树)时很有用。 -
空指针:
nullptr
是C++11引入的空指针字面量,用于表示不指向任何地址的指针。 -
指针类型:指针可以是不同类型的,包括数组指针、函数指针、类成员指针等。
-
指针的算术:指针可以进行加法和减法运算,这通常用于数组和字符串的操作。
-
指针与数组:指针和数组紧密相关,数组名本身就是一个指向数组首元素的指针。
-
动态内存分配:使用
new
和delete
操作符可以在堆上分配和释放内存,这些内存通过指针来访问。 -
指针的初始化:指针在使用前应该被初始化为一个有效的地址或
nullptr
。
6.1变量与指针
指针可以在声明时候赋值,也可以在后期赋值
-
初始化赋值
int i = 100; int *p_ipoint = &i;
-
后期赋值
int i = 100; int p_ipoint ; p_ipoint = &i;
* 和 & 是两种运算符,*表示的是指针运算符,&表示的是取值运算符
6.2指针运算符和取地址运算符
int main(){ //声明两个普通变量 int x, y; //声明两个指针变量,指针变量用来存储变量地址 int *px, *py; //声明一个临时变量,用于交换 int t; //输入两个值,赋值给 x、y。&x 和 &y 是 x 和 y 的地址 //这里为什么在scanf函数中,书写&x,和&y,是因为scanf函数 //是修改变量的内存位置,通过提供变量的内存位置,scanf函 //才能够找到变量才内存中的位置,继而读取数据并存储到该位置 scanf("%d", &x); scanf("%d", &y); //给指针变量 px、py 赋初值(关联变量 x、y) //将指针px指向变量x的地址,将指针py指向变量y的地址 px = &x; py = &y; //*p是解引用操作,表示取出px指针指向的值,即变量x的值 //同理,*py也是取出py指针指向的值 //利用指针来对比 x、y 的值,如果 x 的值比 y 的值小,就交 换 if(*px < *py){ //交换步骤,其中*px == x、*py == y t = *px; *px = *py; *py = t; } printf("x = %d, y = %d", x, y); return 0 ; }
从上述代码可以明白,&和* 的优先级是相同的,按照自右向左的方向结合,所以&*p是先进行星运算,也就是解引用操作,从而获得变量a,再进行&操作,相当获得a变量的所在内存的地址。
而*&a操作是先进行&操作,以获得变量a的地址(&a),再进行星操作,相当于取变量a所在地址的值 (也就是变量a)
6.3指针与数组
6.3.1指针与一维数组
通过指针变量获取数组中的元素
#include <iostream> using namespace std; int main() { int i,a[10]; int *p;//声明一个指针,准备去遍历数组的10个元素 p=&a[0];//指针指向数组第一个元素 for(i=0;i<10;i++) a[i]=i;//给数组赋值 for(i=0;i<10;i++,p++) cout<<"数组元素依次为:"<<*p<<endl; return 0 ; }
上述代码是创建一个一维数组,通过简单赋值,利用指针去遍历数组,然后打印每个数组元素。
6.3.2指针与二位数组
#include <iostream> using namespace std; int main() { int rows = 3; // 二维数组的行数 int cols = 4; // 二维数组的列数 int i, j; int (*arrayPtr)[cols]; // 指向二维数组的指针,指向一个有cols个int元素的数组,而不是指向数组中的单个元素,不同于一般的 int *p , 这里特别强调是指向数组的指针 int (*arrayPtr)[cols],[cols]指定了数组的长度,即这个指针指向一个包含多个int类型元素的数组(cols个元素)。这种命名指针方式特别适用于二维数组 // 使用new动态分配一个二维数组的内存 arrayPtr = new int[rows][cols]; // 使用指针为二维数组赋值 for(i = 0; i < rows; ++i) { for(j = 0; j < cols; ++j) { arrayPtr[i][j] = i * cols + j; // 赋值示例,i*cols + j是生成的一个数字(第一个数组元素的值为0) } } // 使用指针打印二维数组的内容 cout << "二维数组内容: " << endl; for(i = 0; i < rows; ++i) { for(j = 0; j < cols; ++j) { cout << arrayPtr[i][j] << " "; } cout << endl; // 此处为内层for循环结束!!打印完每行后换行去打印下一行 } // 释放分配的内存 delete[] arrayPtr; return 0; }
上述代码所创建的数组示例如下:
0 | 1 | 2 | 3 |
---|---|---|---|
4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 |
6.4函数的指针
函数指针的一般形式:
类型名 * 函数名(参数列表);
int * Function(int x,int y);
若需要用指针函数对数组元素进行求平均值,代码如下:
要求:从键盘输入任意个数的数字(使用vector容器去动态接收键盘输入的数字)
#include<iostream> #include <vector> #include <numeric> using namespace std; // 函数原型声明 //保证vector容器参数不被修改,所以用const去修饰vector double calculateAverage(const vector<int>& numbers); void inputNumbers(vector<int>& numbers); int main() { vector<int> numbers; // 存储输入的数字 double average = 0.0; // 从键盘输入数字 inputNumbers(numbers); // 函数指针声明 double (*calculateAvgPtr)(const vector<int>&) = &calculateAverage; // 使用函数指针调用函数计算平均值 average = calculateAvgPtr(numbers); // 输出平均值 cout << "The average is: " << average << endl; return 0; } // 定义calculateAverage函数,计算并返回数字的平均值 double calculateAverage(const vector<int>& numbers) { if (numbers.empty()) return 0.0; // 如果向量为空,返回0.0 return accumulate(numbers.begin(), numbers.end(), 0.0) / numbers.size(); } // 定义inputNumbers函数,从键盘接收数字输入 void inputNumbers(vector<int>& numbers) { int num; cout << "Enter up to 100 numbers: "; while (cin >> num) { if (numbers.size() >= 100) break; // 不超过100个数字 numbers.push_back(num); } }
6.5指针数组(vector容器)
上面部分利用函数指针去对键盘输入任意个数的数字组成一个数组,然后去求平均值。这里使用vector容器去动态接收键盘输入的任意个数字。
补充内容:vector容器
指针数组的一般形式为: 数据类型 *数组名[数组长度]
int *p[4]; //指针数组的数组名也是一个指针变量,该指针变量为指向指针的指针 int *(*p);//可以写成 int **p
std::vector的特性
-
动态数组:
std::vector
提供了动态数组的功能,可以根据需要自动调整大小。 -
随机访问:
std::vector
提供了对元素的随机访问,这意味着可以通过索引快速访问任何元素。 -
内存管理:
std::vector
自动管理内存,当元素被添加到std::vector
时,它会在必要时自动分配内存,当元素被删除时,它会自动释放内存,所以是动态分配内存 -
类型安全:
std::vector
是模板化的,因此它是类型安全的,只能存储指定类型的元素。 -
容器操作:
std::vector
提供了添加、删除、搜索、排序等容器操作。
第7章:构造数据类型
7.1结构体
结构体定义如下:
struct 结构体类型名
{
成员类型 成员名1;
....
成员类型 成员名n;
}
例如,定义一个员工信息的结构体
struct StuffInfo { int index; int age; char name; int IDnumber; };//注意这里不能遗忘分号;
结构体是一个 构造类型,上面只定义了结构体,形成了一个新的数据类型,但还需要该数据类型来定义变量。结构体变量有两种声明类型。
第一种声明形式是在定义结构体后,使用结构体类型名来声明变量
struct StuffInfo { int index; int age; char name; int IDnumber; }; StuffInfo sinfo;//这里通过结构体来声明了一个sinfo变量
第二种方法是直接在结构体的尾部来声明一个变量
struct StuffInfo { int index; int age; char name; int IDnumber; }sinfo; //在尾部直接声明一个变量
7.2结构体成员及初始化
引用结构体成员的两种方式,一种是声明结构体变量后,通过成员运算符 ”.“ 引用,另一种是声明结构体指针变量,使用指向 ”->" 运算符引用。
-
使用成员运算符 ”.“ ,一般形式如下:
struct StuffInfo { int index; int age; char name; int IDnumber; }sinfo; sinfo.index; sinfo.age; sinfo.name; sinfo.IDnumber;
2.在定义结构体时,可以同时声明结构体指针变量
struct StuffInfo { int index; int age; char name; int IDnumber; }*sinfo; sinfo->index; sinfo->age; sinfo->name; sinfo->IDnumber;
7.3结构体的嵌套
struct WorkPlace //先定义未被嵌套的结构体 { char Address[150]; char PsotGrade[30]; char GateCode[50]; char Street[100]; char Area[50]; } struct PersinInfo //再定义已被定义的结构体 { int index; char name[30]; short age; WorkPlacr myWorkPlace;//此处是被内嵌的结构体 }
7.4结构体与函数
7.4.1结构体变量作函数参数
可以将结构体变量作为普通变量,
#include<iostream> #include <cstring> using namespace std; struct PersonInfo //定义结构体PersonInfo { int index; char name[30];//在c++中,声明一个字符数组(其他数组也一样), 需要提前指定数组长度 short age; }pinfo;//声明变量 //这里自定义一个函数去输出结构体中的变量成员 //ShowStructMessage里面的参数需要 "结构体名称& p",使用引用避免复制 void ShowStructMessage(PersonInfo& pinfo)//PersonInfo类型的引用作为参数 { cout<<"Person's index is:"<<pinfo.index<<endl; cout<<"Person's name is:"<<pinfo.name<<endl; cout<<"Person's age is:"<<pinfo.age<<endl; } int main()//main函数入口 { PersonInfo pinfo;//在这里重新声明PersonInfo 类型的变量 pinfo,是考虑到变量的局部作用域 pinfo.index=1; strcpy(pinfo.name,"XUYUXIA");//利用strcpy来负值字符串 //故需要在头文件声明#include <cstring> pinfo.age=24; ShowStructMessage(pinfo);//传递引用,将pinfo变量的引用传给了上面的ShowStructMessage函数 return 0 ; }
7.4.2结构体指针作函数参数
使用结构体指针变量作为函数参数传递时,只传递的是地址。
#include <iostream> #include <cstring> using namespace std; struct PersonInfo { // 定义结构体PersonInfo int index; char name[30]; short age; }pinfo; // 自定义一个函数去输出结构体中的变量成员 // 使用结构体指针作为参数,需要在参数前加* //将 ShowStructMessage 函数的参数从 PersonInfo& pinfo 改为 PersonInfo* pinfo。这表示现在函数接受一个指向 PersonInfo 类型的指针。 void ShowStructMessage(PersonInfo* pinfo) { // PersonInfo类型的指针作为参数 //在 ShowStructMessage 函数内部,使用 -> 运算符来访问指针指向的结构体的成员。例如,pinfo->index 表示访问指针 pinfo 指向的结构体的 index 成员 cout << "Person's index is: " << pinfo->index << endl; cout << "Person's name is: " << pinfo->name << endl; cout << "Person's age is: " << pinfo->age << endl; } int main() { // main函数入口 PersonInfo pinfo; // 声明PersonInfo类型的变量pinfo pinfo.index = 1; strcpy(pinfo.name, "XUYUXIA"); // 利用strcpy来赋值字符串 pinfo.age = 24; // 传递指针,将pinfo变量的地址传给了ShowStructMessage函数 ShowStructMessage(&pinfo); return 0; }
所以,这是两种方式,需要仔细辨别两种方式在代码的区别。
7.5结构体数组
结构体数组是为了解决多个同类型的结构体成员。
//在定义结构体直接声明 struct PersonInfo { int index; char name[30]; short age; }Person[5];//用Person[]数组去存储,Persong[]数组的5,说明这个结构体数组有5个类似成员,
上面是一种声明方式,下面是第二种声明方式
//使用结构体变量声明 struct PersonInfo { int index; char name[30]; short age; }pinfo;// PersonInfo Person[5]
还有一种代码量较为复杂但直接明了的定义方式,直接在声明结构体数组是对数组进行初始化
struct PersonInfo { int index; char name[30]; short age; }Person[5]{ {1,"zhangsan",20}, {2,"lisi",22}, {3,"wangwu",24}, {4,"songliu",26}, {5,"wangbing",28} };//这里的分号不能忘记,说明Person[5]的数组是包含在结构体PersonInfo中
7.5.1指针访问结构体数组
指针变量可以指向一个数据结构,这时结构指针变量的值是整个结构数组的首地址。结构指针变量也可以指向结构数组的某一个元素,这时的结构指针变量的值就是该元素的存放地址。
#include <iostream> #include <string> using namespace std; struct Person {//定义结构体Person,也可以认为是一种类名,下面main函数的people数组类型为Person,类比 "char name[30]"" string name; int age; }; int main() { //main函数入口 const int MAX_PEOPLE = 3; //定义常量“最大人数” Person people[MAX_PEOPLE] = {//数组名为people,数组类型是Person,数组长度为MAX_PEOPLE = 3,且不可修改 {"Alice", 30}, {"Bob", 25}, {"Charlie", 35} }; // 使用指针遍历并打印数组 //在这里初始化指针*p,确保指针类型必须与其指向的对象类型一致 //当你声明一个指针并将其初始化为指向某个对象时,指针的类型应该与该对象的类型相匹配。 //指针p被声明为Person*类型,他指向Person结构体类型的对象 //people是一个数组,但不能作为对象,因为people是根据结构体Person创建而成的对象集合,不是单一对象,在C++中,对象是类或结构体定义的具体实例。 for (Person* p = people; p < people + MAX_PEOPLE; ++p) { cout << "Name: " << p->name << ", Age: " << p->age << endl; } // 使用指针修改第一个人的年龄 people[0].age = 32; // 等价于 (*people).age = 32 或 people->age = 32 // 再次打印以查看更改 for (Person* p = people; p < people + MAX_PEOPLE; ++p) { cout << "Name: " << p->name << ", Age: " << p->age << endl; } return 0; }
7.6共用体
定义共用体类型的一般形式为:
union 共用体类型名 { 成员类型 共用体类型名1; 成员类型 共用体类型名2; ... 成员类型 共用体类型名3; }
声明共用体数据类型变量有两种方式
第一种:
//先定义共用体,然后声明共用体变量 union myUnion//定义共用体类型名为myUnion { int i; char ch; float f; }; myUnion mu;//通过共用体来声明一个mu变量,类型名为myUnion
第二种:
//直接在定义时声明共用体变量 union myUnion { int i; char ch; float f; }mu;
共用体的特点:
-
使用共用体变量的目的是希望用同一个内存段存放几种不同数据类型的数据,但是特别注意,在每一个瞬时只能存放其中一种,而不是同时存放多种。
-
能够访问的是共用体变量中的最后一次被赋值的成员,在对一个新的成员赋值后原有的成员就失去作用。
-
共用体变量的地址(例如mu的地址)与共同体结构中的成员地址都是同一地址。
-
不能对共用体变量(mu)赋值;不能企图引用变量名来得到一个值;不能再定义共用体变量时进行初始化;不能用共用体变量名作为函数参数。(不同于结构体变量名)
7.7枚举类型的声明
枚举类型的定义有两种声明形式:
(1)枚举类型的一般形式
enum 枚举类型名{标识符列表};
enum weekday{Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday}; //enum 是定义枚举类型的关键字,weekday是被新定义的类型名,花括号内就是枚举类型变量应取的值。
(2)带赋值的枚举类型声明形式
enum 枚举类型名 { 标识符[=整型常量], 标识符[=整型常量], ... 标识符[=整型常量], }枚举变量; 例如: enum weekday{Sunday=0,Monday=1,Tuesday=2,Wednesday=3,Thursday=4,Friday=5,Saturday=6};
7.8自定义数据类型
typedef的使用形式如下:
typedef <原类型名><新类型名>
原类型名是任意已定义的数据类型,包括系统的各种数据类型名以及用户自定义的构造类型名
在C++中,自定义数据类型通常指的是通过用户定义的类(class
)或结构体(struct
)来创建的类型。这些自定义类型允许你封装数据和行为,创建具有特定属性和方法的复杂数据结构。以下是一些创建和使用自定义数据类型的常见方法:
1.结构体(struct):
-
结构体是C++中一种简单的自定义数据类型,由一系列不同类型的成员变量组成。
-
结构体通常用于存储数据,但不包含方法。
struct Point { float x; float y; };
2.类(class)
-
类是面向对象编程中的核心概念,它允许你定义具有私有和公共成员的数据类型。
-
类可以包含数据成员(属性)和成员函数(方法),支持封装、继承和多态。
class Rectangle { private: float width; float height; public: void setDimensions(float w, float h) { width = w; height = h; } float area() const { return width * height; } };
3.枚举(enum)
枚举是用户定义的整型常数集合,用于定义一组命名的整数值。
enum Color { RED, GREEN, BLUE };
4.联合(union)
联合是一种特殊的自定义数据类型,允许在同一内存位置存储不同的数据类型。
union Data { int i; float f; char* s; };
5.类型别名(using或typedef)
使用 using
或 typedef
关键字可以为现有类型创建别名,简化类型声明。
using IntArray = std::vector<int>; typedef std::map<std::string, int> StringToIntMap;
6.模板(template)
模板是C++中的泛型编程工具,允许你创建使用任意类型作为数据类型的类、函数或联合。
template <typename T> class Stack { private: std::vector<T> elements; public: void push(const T& element) { elements.push_back(element); } T pop() { T elem = elements.back(); elements.pop_back(); return elem; } };
7.智能指针(std::unique_ptr
、std::shared_ptr
):
智能指针是C++标准库中的自定义数据类型,用于自动管理动态分配的内存
std::unique_ptr<int> uniqueInt(new int); std::shared_ptr<int> sharedInt(new int);
8.自定义类型转换
可以通过重载类型转换运算符来定义自定义类型到基本类型的隐式或显式转换。
class Degree { private: double angle; public: operator double() const { return angle; } };
第8章:面向对象编程
8.1面向对象概述
(文字叙述,不多展开手敲,直接抠图)
8.2面向对象与面向过程
8.3统一建模语言
第9章:类和对象
9.1类
类的声明格式如下:
class 类名标识符 { [public:] [数成员的声明] [成员函数的声明] [private:] [数成员的声明] [成员函数的声明] [protected:] [数成员的声明] [成员函数的声明] }
类的声明格式的说明:
-
class是定义类结构体的关键字,花括号内被称为类体或者类空间
-
类名标识符指定的就是类名,类名就是一个新的数据类型,通过类名可以声明类下面的对象
-
类的成员包括函数和数据两种类型
-
花括号内是定义和声明类成员的地方,关键字public,private,protected是类成员访问的修饰符
举例:
class Cperson { /*属性:数据成员*/ int m_iIndex; //声明数据成员 char m_cName; short m_shAge; double m_dSalary; /*行为:成员函数*/ int getAge(); //声明成员函数 int setAge(shrot sAge); int getIndex(); int setIndex(int iIndex); double getSalary(); int setSalary(double dSalary); }
类的实现
-
第一种方法是将类的成员函数都定义在类体内
-
第二种方法,可以将类体内的成岩函数的实现放在类体外,但如果类成员函数定义在类体外,需要用到域运算符 “::”,放在类体内和类体外的效果是一样的‘’‘
并且,实现文件放入函数的实现,也就是.cpp后缀,而头文件放入函数声明,也就是. h文件。
类的实现两点说明:
-
类的数据成员需要初始化,成员函数还需要添加实现代码。类的数据成员不可以再类的声明中初始化
-
空类是c++中最简单的类,声明方式如下:
空类只是起到占位作用,需要的时候在定义类成员及实现
对象的声明
声明形式如下:
类名 对象名表 // CPerson p; 声明单对象
// CPerson p1,p2.p3; 声明多个对象
对象的引用方式包括两种,一种是成员引用方式,一种是对象指针方式。
成员引用方式(通过对象直接访问)
-
直接访问:使用点(
.
)运算符直接通过对象访问成员。 -
语法:
object.member
-
适用情况:当对象是活动的,并且你知道它是一个有效的实例。
-
示例:
Person p; p.age = 25;
-
记忆提示:点运算符就像在“点名”对象的成员。
举例 :
CPerson p; p.m_iIndex; p.getIndex;
对象指针方式(通过指针访问)
-
间接访问:使用箭头(
->
)运算符通过指针访问对象的成员。 -
语法:
pointer->member
-
适用情况:当操作符作用于指针类型变量,或者需要通过指针来解引用对象时。
-
示例:
Person* p = &Person; p->age = 25;
-
记忆提示:箭头运算符表示“指向”对象的成员,像箭一样“射向”成员。
CPerson* p; (*p).m_iIndex;//对类中的成员进行引用 p->m_iIndex;//对类中的成员进行引用
很好记忆,如果代码中已经有一个明确的对象实例,使用点运算符;如果处理的是对象的地址或者指针,则使用箭头运算符。
9.2构造函数
构造函数的特点:
-
名称:构造函数的名称必须与类或结构体的名称完全相同。
-
参数:构造函数可以有参数,也可以没有参数。参数允许在创建对象时传递初始化数据。
-
返回类型:构造函数没有返回类型,甚至连
void
也不是。 -
作用:构造函数的主要作用是初始化对象的成员变量,设置对象的初始状态。
-
自动调用:每当创建一个对象时,构造函数会自动被调用,无需程序员显式调用。
-
重载:可以为同一个类定义多个构造函数,这些构造函数具有不同的参数列表,这称为构造函数重载。
-
默认构造函数:如果没有为类定义任何构造函数,编译器会自动生成一个默认构造函数。如果类中有成员变量已经被初始化了,则生成的默认构造函数会使用这些初始化值。
-
析构函数:与构造函数相对应,析构函数在对象生命周期结束时被调用,用于执行清理工作。
-
初始化列表:C++提供了一种称为成员初始化列表的特性,允许在构造函数体内直接初始化成员变量。
-
常量成员函数:如果构造函数将类的对象设置为不可修改的状态,它可以被声明为常量成员函数。
class Person { public: std::string name; int age; // 构造函数声明 Person(const std::string& name, int age); }; // 构造函数定义 Person::Person(const std::string& name, int age) { this->name = name; // 初始化成员变量name this->age = age; // 初始化成员变量age }
同样,构造函数包括默认构造函数和带参构造函数
//默认构造函数 //构造函数中不包含参数,称为默认构造函数 class CPerson { public: CPerson(); //默认构造函数 int m_iIndex; int getIndex(); }; //构造函数 CPerson ::CPerson() { m_iIndex=10; }
-
带有一个或多个参数的构造函数,用于在创建对象时提供初始化数据。
-
带参构造函数允许你根据提供的参数值来初始化对象的状态。
//带参构造函数 class CPerson { public: Cperson(int); //构造函数 int m_iIndex; int getIndex(); }; //构造函数 CPerson ::CPerson() { m_iIndex=iIndex; }
9.3析构函数
在C++中,析构函数是一种特殊的成员函数,它的主要目的是在对象生命周期结束时进行清理工作。以下是析构函数的一些关键特性和作用:
-
目的:析构函数的主要目的是释放对象在生命周期中分配的资源,包括动态内存、文件句柄、网络连接等。
-
名称:析构函数的名称与类名相同,但在类名前面加上一个波浪号(
~
)作为前缀。 -
返回类型:析构函数没有返回类型,甚至连
void
也不是。 -
自动调用:当对象生命周期结束时,如离开作用域、被
delete
操作符删除或程序结束时,析构函数会被自动调用。 -
资源管理:析构函数通常用于释放对象占用的资源,防止内存泄漏和其他资源泄露。
-
继承:如果一个类继承自另一个类,基类的析构函数会被派生类的析构函数调用,以确保正确地释放基类分配的资源。
-
虚析构函数:在具有虚函数的类中,将析构函数声明为虚函数(
virtual
)是很重要的。这确保了当通过基类指针删除派生类对象时,正确的析构函数被调用。 -
构造函数和析构函数配对:构造函数用于初始化对象,而析构函数用于销毁对象。它们通常成对出现,以确保对象被正确创建和清理。
-
执行顺序:对于对象的创建和销毁,构造函数首先被调用,然后是析构函数。
-
多重继承中的析构函数:在多重继承的情况下,每个基类的析构函数都会被调用,按照从左到右的顺序,与它们在类声明中的顺序相反。
class MyClass { public: MyClass() { // 构造函数代码 } ~MyClass() { // 析构函数代码,用于清理资源 std::cout << "MyClass对象被销毁" << std::endl; } }; int main() { MyClass obj; // 创建对象,构造函数被调用 // obj生命周期结束,析构函数被自动调用 return 0; }
在例子中,MyClass
有一个构造函数和一个析构函数。当main
函数中创建的obj
对象生命周期结束时,析构函数会被自动调用,输出一条消息表示对象被销毁。
注意事项!!
-
一个类中只能定义一个析构函数
-
析构函数不能重载
-
构造函数和析构函数不能使用return语句返回值。不用加上关键字 void
构造函数和析构函数的调用环境
-
自动变量的作用域是某个模块,当此模块被激活时,自动变量调用构造函数,当退出此模块时,会调用析构函数。
-
全局变量在进入main()函数之前会调用构造函数,在程序种植会调用析构函数
-
动态分配的对象在使用new为对象分配内存时会调用构造函数;使用delete删除对象时会调用析构函数
9.4类成员函数
9.4.1访问类成员
类成员可以是任何有效的C++类型,包括基本数据类型,其他类的对象、指针、引用等。关键字public、private、protected分别表示类成员是共有的、私有的,还是保护的。public和private好理解,protected成员可以在类及派生类中访问。
9.4.2内联成员函数
在定义函数,可以使用inline关键字将函数定义为内联函数。在定义类的成员函数时,也可以使用inline关键字将成员定义为内联成员函数。
9.4.3静态(动态)类成员
静态成员函数:
-
类相关:静态成员函数属于类本身,而不是类的任何特定实例。
-
无
this
指针:静态成员函数中没有this
指针,因为它们不与类的实例关联。 -
访问类成员:只能访问类的静态成员和常量,因为它们不依赖于任何特定对象的状态。
-
重载:可以被重载,但重载规则与非静态成员函数略有不同。
-
继承:静态成员函数不能被派生类重写,但它们可以通过继承在派生类中使用。
动态成员函数(非静态成员函数):
-
实例相关:动态成员函数与类的实例相关联,每个实例都有自己的副本。
-
访问
this
指针:动态成员函数可以访问this
指针,它指向调用函数的对象。 -
访问所有成员:可以访问类的私有(
private
)、受保护(protected
)和公开(public
)成员。 -
重载和重写:可以被重载(在同一类中具有相同名称但参数不同的函数)和重写(在派生类中重写基类的虚函数)。
简单来说,静态成员函数和动态成员函数的主要区别是声明这个过程,静态成员函数是定好的,而动态成员函数是依情况而定的,需要根据你在main函数中创建的实例来使用动态成员函数。
这段代码就能很好的例子来说明动态成员函数来调用实例
class Person { public: std::string name; int age; void Introduce() {//定义introduce成员函数,属于Person类 std::cout << "Hello, my name is " << name << " and I am " << age << " years old." << std::endl; } }; int main() { Person alice; // 创建一个Person实例 alice.name = "Alice"; alice.age = 30; alice.Introduce(); // 调用实例的动态成员函数 Person bob; // 创建另一个Person实例 bob.name = "Bob"; bob.age = 24; bob.Introduce(); // 调用另一个实例的动态成员函数 return 0; }
在main函数中,创建了两个实例,两个实例分别调用动态成员函数
9.4.4隐藏this指针
this指针:
在C++中,this
指针是一个特殊的指针,它在每个非静态成员函数内部自动可用。this
指针指向调用成员函数的那个对象的地址。以下是 this
指针的一些关键特性:
-
隐式可用:在类的非静态成员函数中,
this
指针是自动传递的,不需要程序员显式声明或传递。 -
指向对象:
this
指针指向调用成员函数的对象。对于静态成员函数,不存在this
指针,因为它们不与类的任何特定实例关联。 -
类型:
this
指针的类型是指向类类型的指针。例如,如果类名为MyClass
,则this
的类型是MyClass*
。 -
常量性:在常量成员函数中,
this
指针是指向常量的指针,即const MyClass*
。 -
用途:
-
访问成员变量:当成员变量被隐藏或覆盖时,使用
this
指针可以明确访问当前类的成员变量。 -
调用其他成员函数:可以在成员函数内部使用
this
指针调用类的其他成员函数。 -
区分同名成员:在多继承情况下,
this
指针可以用来区分同名成员函数。
隐藏this指针:
隐藏
this
指针使用的方法:-
私有继承:当你通过私有继承一个基类时,派生类无法访问基类的
this
指针。这可以看作是一种隐藏基类this
指针的方式。 -
封装:在类的公有接口中不提供直接访问成员变量或成员函数的方法,而是通过私有成员和受保护的成员来控制对类数据的访问。
-
使用静态成员函数:静态成员函数不接收
this
指针,因为它们不与类的实例关联。如果你的类设计中使用了很多静态函数,这可以减少对this
指针的依赖。 -
友元类和友元函数:虽然友元可以访问类的私有成员,但它们不会接收
this
指针。通过使用友元类或友元函数,你可以控制对this
指针的访问。 -
限制对象的复制:通过使复制构造函数和赋值操作符为私有,可以防止对象的复制,从而减少
this
指针被外部访问的可能性。 -
最小化公有接口:设计类时,尽量减少公有成员函数的数量,这样可以减少外部代码直接通过
this
指针访问类成员的机会。 -
使用
delete
关键字:你可以在类中使用delete
关键字来删除特定的成员函数,包括那些可能暴露this
指针的操作。 -
使用
final
关键字:在C++11中,你可以使用final
关键字来阻止类的进一步继承。这样,即使this
指针在派生类中可见,也无法通过继承来扩展类的功能。 -
隐藏实现细节:通过将实现细节放在类的私有部分或单独的源文件中,可以减少外部代码对
this
指针的直接访问。
-
9.4.5嵌套类和局部类
嵌套类(也称为内部类)是定义在另一个类内部的类。使用嵌套类时,需要注意以下几点:
-
作用域:嵌套类的作用域限定在其外部类中。这意味着外部类的成员可以访问嵌套类的成员,包括私有成员。
-
访问控制:嵌套类可以访问外部类的所有成员,包括私有成员,但外部类不能直接访问嵌套类的私有成员。
-
实例化:嵌套类的实例化需要通过外部类的实例。不能在外部类的外部直接实例化嵌套类的对象。
-
名称解析:当嵌套类在外部类外部被使用时,需要通过外部类的实例来访问嵌套类的成员。
-
成员函数:嵌套类可以有自己的成员函数,这些函数可以访问外部类的所有成员。
-
静态成员:嵌套类可以拥有静态成员,但这些静态成员不属于外部类的任何特定实例。
-
友元关系:嵌套类不能自动成为外部类的友元,即使它们可以访问外部类的私有成员。
-
继承:嵌套类可以继承其他类,也可以被其他类继承。
-
多重继承:如果嵌套类继承了多个基类,需要考虑多重继承带来的复杂性和菱形继承问题。
-
模板类:嵌套类可以是模板类,允许外部类具有类型参数化的内部类。
-
命名空间:嵌套类在命名空间中的作用域是受限的,通常它们不会影响外部的命名空间。
-
编译器支持:所有现代C++编译器都支持嵌套类,但不同编译器可能在语法和行为上有所不同。
-
嵌套类的特殊性:嵌套类可以访问外部类的所有成员,但反过来,外部类的成员函数需要明确地声明它们对嵌套类成员的访问意图,尤其是如果嵌套类的成员是私有的时候。
-
class OuterClass { private: int outerPrivate;//声明外部类的私有变量 public: void function() { // 声明成员函数 } class InnerClass { private: int innerPrivate; public: void display() { // 嵌套类的成员函数可以访问外部类的所有成员 std::cout << outerPrivate << std::endl; } }; }; int main() { OuterClass outer; OuterClass::InnerClass inner; // 若需要调用内部类的成员函数,需要先通过外部类实例化嵌套类,实例化就是为嵌套类创建inner对象 inner.display(); // 调用嵌套类的成员函数 return 0; }
9.5友元
友元(Friend)是一种特殊的类成员或函数,它不属于类的私有或受保护成员,但被授权可以访问类的私有(private
)和受保护(protected
)成员。友元提供了一种在类之间共享访问权限的方式,即使这些类之间没有继承关系。
友元的主要特点包括:
-
访问权限:友元可以访问类的所有成员,包括私有和受保护的成员。
-
非成员:友元函数不是类的成员函数,但它可以像类的成员函数一样访问类的内部数据。
-
声明方式:友元通过在类的定义中使用
friend
关键字来声明。 -
不继承:友元关系不是继承关系,它不涉及类的层次结构。
-
单一关系:友元关系是单向的,如果类A是类B的友元,这并不意味着类B自动成为类A的友元,除非明确声明。
-
类模板:友元可以是模板类或模板函数,这允许它们访问多个类的私有成员。
-
命名空间:友元关系不受命名空间的限制,即使类在不同的命名空间中。
-
友元类:一个类可以声明另一个类作为友元,这样第二个类的所有成员函数都可以访问第一个类的所有成员。
-
友元函数:一个函数可以被声明为类的友元,这样它可以访问类的私有和受保护成员。
友元函数:
class MyClass { private: int privateData; public: friend void accessPrivateData(MyClass& obj); }; void accessPrivateData(MyClass& obj) { // 直接访问obj的私有数据 std::cout << obj.privateData << std::endl; }
友元类:
class MyClass { private: int privateData; public: friend class MyFriendClass; }; class MyFriendClass { public: void accessPrivateData(MyClass& obj) { // 直接访问obj的私有数据 std::cout << obj.privateData << std::endl; } };
9.6命名空间
命名空间(Namespace)是一种将程序中的实体(如变量、函数、类等)组织在一起的机制。命名空间的主要目的是解决名称冲突问题(应用程序内的多个文件存在同名的全局对象,这样会导致应用程序的连接错误,可以使用命名空间来消除命名冲突的最佳方式),并提供了一种将逻辑相关的实体分组的方法。以下是命名空间的一些关键特性:
-
定义:使用关键字
namespace
定义一个新的命名空间。 -
作用域:命名空间提供了一个作用域,其中的实体不会与同一程序中的其他实体冲突。
-
声明:在命名空间中声明的实体只有在使用
using
声明或者命名空间别名后才可在外部访问。 -
嵌套:命名空间可以嵌套定义,即一个命名空间内部可以包含另一个命名空间。
-
标准库:C++标准库中的所有名称都定义在
std
命名空间中。 -
无限制长度:命名空间的名称可以是任意长度的标识符。
-
成员访问:使用作用域解析运算符
::
来访问命名空间的成员。 -
using声明:使用
using
关键字可以引入特定的命名空间成员,使其在当前作用域内可见。 -
using指令:使用
using namespace
指令可以引入整个命名空间,使该命名空间内的所有名称在当前作用域内可见。 -
别名:可以使用
namespace
后面跟别名来为现有的命名空间定义一个新的名称。
// 定义一个命名空间 namespace MyNamespace { int value = 10; void function() { std::cout << "Function in MyNamespace" << std::endl; } } // 使用命名空间成员函数 MyNamespace::function(); // 使用using声明 using MyNamespace::value; std::cout << value << std::endl; // 使用using指令 using namespace MyNamespace; function(); // 直接调用,不需要命名空间名称 // 定义命名空间别名 namespace MN = MyNamespace; MN::function();
第10章:继承与派生
10.1继承
继承是一种机制,它允许一个类(称为派生类或子类)继承另一个类(称为基类或父类)的属性和方法。继承是面向对象编程的核心概念之一,它支持代码复用、多态性和层次结构的建立。以下是继承的一些关键特性:
-
代码复用:派生类可以复用基类的代码,无需重新编写相同的代码。
-
扩展性:派生类可以扩展基类的功能,添加新的属性和方法。
-
多态性:通过虚函数,派生类可以重写基类的方法,实现多态性。
-
访问控制:派生类可以访问基类的公共(
public
)和受保护(protected
)成员,但不能直接访问私有(private
)成员。 -
构造和析构:派生类的构造函数和析构函数需要正确地调用基类的构造函数和析构函数,以确保对象的正确初始化和清理。
-
继承方式:
-
公有继承(
public
):基类的公共成员和受保护成员在派生类中分别作为公共成员和受保护成员。 -
保护继承(
protected
):基类的公共成员和受保护成员在派生类中都作为受保护成员。 -
私有继承(
private
):基类的公共成员和受保护成员在派生类中都作为私有成员。
-
-
多重继承:C++允许类从多个基类继承,但多重继承可能导致复杂的设计问题。
-
虚继承:用于解决多继承中的菱形继承问题,确保每个类只有一个基类的实例。
-
接口继承:派生类可以继承基类的接口(即虚函数),但不必立即提供实现。
-
实现继承:派生类继承并重写基类的方法实现。
class Base { public: virtual void show() { std::cout << "Base show" << std::endl; } }; class Derived : public Base { public: void show() override { std::cout << "Derived show" << std::endl; } }; int main() { Derived d; d.show(); // 调用派生类的show() Base* b = &d; b->show(); // 多态性调用,实际执行Derived类的show() return 0; }
10.2多重继承
多重继承是C++中的一个特性,它允许一个类(称为派生类或子类)从多个基类继承属性和方法。这意味着派生类可以同时获得多个基类的行为和特征。然而,多重继承也带来了一些复杂性和设计挑战。
多重继承的特点:
-
语法:使用冒号
:
和访问说明符(如public
,protected
,private
)来指定对每个基类的继承方式。 -
菱形继承:当两个基类有一个共同的基类时,多重继承可能导致所谓的“菱形继承”问题,其中一个类继承了两次相同的基类成员。
-
二义性:在某些情况下,如果多个基类中有同名的成员,可能会导致二义性,派生类需要明确指定使用哪个基类的成员。
-
构造和析构:派生类的构造函数和析构函数需要正确地调用所有基类的构造函数和析构函数。
-
虚拟继承:使用虚拟继承可以解决菱形继承问题,确保每个基类只有一个实例,避免重复继承。
-
成员访问:派生类可以访问所有基类的公共和受保护成员,但私有成员不可见。
-
类型转换:在多重继承中,类型转换可能会变得复杂,因为存在多个基类类型。
-
设计复杂性:多重继承增加了设计的复杂性,可能导致难以理解和维护的代码。
class Base1 { public: int value1; }; class Base2 { public: int value2; }; class Derived : public Base1, public Base2 { public: // Derived类从Base1和Base2继承 }; int main() { Derived d; d.value1 = 10; // 访问Base1的成员 d.value2 = 20; // 访问Base2的成员 return 0; }
特别注意:
-
多重继承可能带来运行时性能开销,因为需要处理多个基类的构造和析构。
-
多重继承可能导致设计上的混乱,特别是当基类之间存在功能重叠时。
-
虚拟继承是解决菱形继承问题的一种方法,但它也可能引入额外的复杂性。
10.3多态
多态(Polymorphism)是面向对象编程(OOP)的一个核心概念,它指的是同一个接口可以被不同的数据类型以不同的方式实现或表示。在C++中,多态主要通过以下几种方式实现:
-
虚函数(Virtual Functions):
-
基类中使用
virtual
关键字声明的函数可以被子类重写(Override)。 -
通过基类的指针或引用调用虚函数时,将根据对象的实际类型调用相应的函数,这称为动态绑定或晚期绑定。
-
-
重写(Overriding):
-
派生类中使用
override
关键字(C++11引入)明确指出要重写基类中的虚函数。
-
-
纯虚函数(Pure Virtual Functions):
-
如果虚函数没有实现,并且被声明为
= 0
,则称为纯虚函数。 -
包含纯虚函数的类称为抽象类(Abstract Class),不能被直接实例化。
-
-
接口继承:
-
派生类可以继承并实现基类的接口,即使不修改基类的方法实现。
-
-
动态类型识别:
-
使用
dynamic_cast
运算符可以在运行时安全地将基类指针转换为派生类指针,前提是存在继承关系并且至少有一个虚函数。
-
-
虚函数表(VTable):
-
C++通过虚函数表来实现多态,每个包含虚函数的类都有一个虚函数表。
-
-
多态的使用场景:
-
当你希望编写通用的代码,它可以与多种不同类型的对象一起工作时,多态非常有用。
-
在设计模式中,如工厂模式、策略模式等,多态是实现这些模式的关键。
class Shape { //父类Shape public: virtual void draw() const { std::cout << "Drawing a generic shape." << std::endl; } virtual ~Shape() {} // 虚析构函数确保派生类的析构函数被调用 }; class Circle : public Shape {// Circle类 //继承Shape基类 public: void draw() const override { std::cout << "Drawing a circle." << std::endl; //成员函数重写,重写基类Shape中的虚函数draw //提示,派生类重写基类的函数,只能重写虚函数 } }; int main() { //声明一个名为shapes的std::vector容器,用于存储指向shape类型的指针 std::vector<Shape*> shapes; //shapes.push_back(new Circle()); 使用 new 运算符为 Circle 类的对象分配内存,并将指向该对象的指针添加到 shapes 容器中 shapes.push_back(new Circle()); //同样为 Shape 类的对象分配内存,并将指针添加到容器中。 shapes.push_back(new Shape()); for (const Shape* shape : shapes) { shape->draw(); // 多态调用,根据对象的实际类型调用相应的draw() } // 清理分配的内存 for (Shape* shape : shapes) { delete shape; } return 0; }
10.4抽象类
-
抽象类是一种不能被直接实例化的类,它通常被用作基类,为派生类提供公共的接口或实现。抽象类通过包含至少一个纯虚函数(pure virtual function)来实现。以下是抽象类的关键特性:
-
纯虚函数:抽象类中至少包含一个纯虚函数。纯虚函数没有实现,使用
= 0
语法声明。 -
不能实例化:由于包含纯虚函数,抽象类不能被直接实例化。尝试实例化抽象类将导致编译错误。
-
派生类实现:从抽象类派生的派生类必须实现所有继承的纯虚函数,除非派生类也是抽象类。
-
接口定义:抽象类通常用于定义一个接口或一组规范,派生类遵循这些规范来实现具体的行为。
-
虚析构函数:虽然不是必需的,但通常建议在抽象类中声明虚析构函数,以确保派生类的析构函数被正确调用。
-
受保护的成员:抽象类通常包含受保护的成员,这些成员可以在派生类中访问和修改,但不能在抽象类外部访问。
-
多态性:抽象类通过虚函数支持多态性,使得基类指针或引用可以指向派生类对象,并调用派生类中重写的方法。
-
设计目的:抽象类主要用于设计目的,为派生类提供一个共同的基类,定义它们必须遵循的接口。
class Shape { public: // 纯虚函数,没有实现 //这种"= 0;"语法,在函数声明末尾使用,表示这是一个纯虚函数,没有实现 virtual void draw() const = 0; // 虚析构函数 virtual ~Shape() {} }; // 正确的做法:不要直接实例化抽象类 // Shape shape; // 错误:无法实例化Shape //派生类Circle 类继承Shape基类 class Circle : public Shape { public: //派生类重写基类draw()函数 void draw() const override { std::cout << "Drawing a circle." << std::endl; } }; int main() { // 使用抽象类的指针或引用来操作派生类对象 //shape指针指向一个Circe类对象 Shape* shape = new Circle(); shape->draw(); // 多态性调用Circle类的draw() delete shape; return 0; }
在这个示例中,Shape
类是一个抽象类,因为它包含一个纯虚函数 draw()
。由于 Shape
是抽象类,我们不能直接实例化它。然而,我们可以创建一个指向 Shape
类型的指针 shape
,并让它指向 Circle
类的对象。通过这个指针,我们可以调用 Circle
类中重写的 draw()
函数,展示了多态性.
第11章:模板
11.1函数模板
函数模板的一些关键特性:
-
模板声明:使用
template
关键字和尖括号<>
来声明模板参数。 -
类型参数:在模板的尖括号内,你可以定义一个或多个类型参数,如
typename T
或class T
。 -
函数定义:在模板参数之后,紧跟着函数的返回类型、名称和参数列表。
-
泛型编程:函数模板允许你编写泛型代码,这些代码可以与多种数据类型一起工作,而不需要为每种类型编写特定的函数。
-
模板实例化:当你使用特定的类型调用函数模板时,编译器会生成一个该类型的函数实例。
-
重载:函数模板可以与普通函数一起重载,允许你为不同的数据类型提供不同的实现。
-
非类型参数:除了类型参数,函数模板还可以接受非类型参数(如整数或常量表达式)。
-
默认参数:模板参数可以有默认值,这样在调用时可以省略这些参数。
-
模板特化:你可以为特定的类型或非类型参数提供函数模板的特化版本。
-
编译时多态:函数模板在编译时解决多态性,这意味着编译器会根据调用的上下文生成最适合的代码。
#include <iostream> // 函数模板声明 template <typename T> T max(T a, T b) { return (a > b) ? a : b; } int main() { std::cout << "Max of 5 and 10: " << max(5, 10) << std::endl; std::cout << "Max of 3.14 and 2.71: " << max(3.14, 2.71) << std::endl; std::cout << "Max of 'a' and 'z': " << max('a', 'z') << std::endl; return 0; }
在这个示例中,max
函数模板接受两个类型相同的参数,并返回两者中的最大值。由于模板的泛型特性,max
函数可以处理整数、浮点数、字符等不同类型的参数。
11.2类模板
类模板的关键特性:
-
模板声明:使用
template
关键字和尖括号<>
来声明模板参数。 -
类型参数:在模板的尖括号内,你可以定义一个或多个类型参数,如
typename T
或class T
。 -
类定义:在模板参数之后,紧跟着类名称和类成员的定义。
-
泛型类:类模板允许你创建泛型类,这些类可以与多种数据类型一起工作,而不需要为每种类型编写特定的类。
-
模板实例化:当你使用特定的类型实例化类模板时,编译器会生成一个该类型的类实例。
-
模板特化:你可以为特定的类型或非类型参数提供类模板的特化版本。
-
成员函数和模板:类模板的成员函数也可以是模板,允许进一步的泛型编程。
-
默认参数:模板参数可以有默认值,这样在声明或实例化类时可以省略这些参数。
-
友元声明:类模板可能需要声明友元,以允许非成员函数访问类的私有和受保护成员。
-
继承和模板:类模板可以继承自其他类模板或普通类,并且可以被其他模板或普通类继承。
#include <iostream> #include <vector> #include <string> // 类模板声明 template <typename T> class Container { private: std::vector<T> elements; public: void add(const T& element) { elements.push_back(element); } void display() const { for (const auto& element : elements) { std::cout << element << " "; } std::cout << std::endl; } }; int main() { Container<int> intContainer; intContainer.add(1); intContainer.add(2); intContainer.display(); // 输出: 1 2 Container<std::string> stringContainer; stringContainer.add("Hello"); stringContainer.add("World"); stringContainer.display(); // 输出: Hello World return 0; }
在这个示例中,Container
是一个类模板,它使用类型参数 T
来允许存储任何类型的元素。我们为 Container
类模板定义了两个成员函数:add
用于添加元素,display
用于显示所有元素。在 main
函数中,我们分别使用 int
和 std::string
类型来实例化 Container
类模板。
11.3模板的使用
函数模板:函数模板允许你定义一个函数,该函数能够接受不同类型的参数并对其进行操作。使用函数模板时,你不需要为每种想要支持的类型创建不同的函数版本。编译器在调用时根据传递的参数类型自动生成相应的函数实例。
类模板:类模板允许你定义一个类,该类能够拥有不同类型的成员变量,并且可以对这些不同类型的数据执行操作。使用类模板时,你可以创建类的实例,这些实例的成员变量和成员函数可以根据传递给模板的类型参数而变化。
如何理解模板:
-
泛型编程:模板提供了一种编写泛型代码的方法,这种代码不依赖于特定的数据类型。
-
类型参数:模板使用类型参数(如
typename T
)来允许不同的数据类型。 -
参数化:模板定义了一组规则,这些规则在实例化时由具体的类型参数填充。
-
实例化:当使用特定的类型调用模板时,编译器生成一个实例,这个过程称为实例化。
-
模板特化:你可以为特定的类型或值提供模板的特定实现,这称为特化。
-
编译时多态:模板在编译时解决类型问题,而不是在运行时。
如何使用模板:
-
定义模板:首先,你需要定义一个模板,指定类型参数,并在函数或类的定义中使用这些参数。
-
实例化模板:在调用模板时,你可以指定所需的类型,或者让编译器根据上下文推断类型。
-
使用模板参数:在模板定义中,使用类型参数来声明变量、定义函数或创建类的成员。
-
模板重载:你可以为同一个模板定义多个版本,每个版本接受不同数量或类型的参数。
-
模板特化:对于特定类型,你可以提供模板的特化版本,以提供定制的行为。
-
友元和模板:如果需要,你可以声明友元函数或类,以便它们可以访问模板类的私有成员。
-
嵌套模板:类模板可以包含函数模板,或者函数模板可以创建类模板的实例。
11.4链表类模板
链表模板是使用模板编程创建的通用数据结构,它可以独立于特定的数据类型。链表模板允许你创建存储任何类型元素的链表,实现代码的复用和类型安全性。
#include <iostream> #include <memory> // For std::unique_ptr // 链表节点模板 template <typename T>//模板声明,模板参数T class ListNode { public: T value; // 节点存储的值 ListNode* next; // 指向下一个节点的指针 // 构造函数 ListNode(T val) : value(val), next(nullptr) {} }; // 链表模板 template <typename T> class LinkedList { private: ListNode<T>* head; // 链表头部的指针 public: // 构造函数 LinkedList() : head(nullptr) {} // 析构函数,释放链表内存 ~LinkedList() { ListNode<T>* current = head; while (current != nullptr) { ListNode<T>* next = current->next; delete current; current = next; } } // 在链表末尾添加元素 void append(T value) { ListNode<T>* newNode = new ListNode<T>(value); //判断链表为空,则将新节点设为头节点 if (head == nullptr) { head = newNode; } else { ListNode<T>* current = head; while (current->next != nullptr) { current = current->next; } current->next = newNode; } } // 打印链表中的所有元素 void print() const { ListNode<T>* current = head; while (current != nullptr) { std::cout << current->value << " "; current = current->next; } std::cout << std::endl; } }; int main() { LinkedList<int> intList; intList.append(1); intList.append(2); intList.append(3); std::cout << "Int List: "; intList.print(); LinkedList<std::string> stringList; stringList.append("Hello"); stringList.append("World"); std::cout << "String List: "; stringList.print(); return 0; }
-
ListNode
是一个节点模板,用于存储链表中的每个元素。 -
LinkedList
是链表模板,包含对链表进行操作的方法,如添加元素到末尾 (append
) 和打印所有元素 (print
)。 -
链表模板使用模板参数
T
来允许存储任何类型的数据。 -
在
main
函数中,我们分别创建了存储整数和字符串的链表,并展示了如何使用链表模板。
链表模板通过提供类型参数 T
,使得 ListNode
和 LinkedList
类可以处理任何数据类型的节点和元素。这使得链表成为一个非常灵活的数据结构,可以适应不同的使用场景。
使用模板编写的链表具有以下优点:
-
类型安全性:模板确保了存储在链表中的元素类型的一致性。
-
内存管理:析构函数负责释放链表分配的所有节点,防止内存泄漏。
-
代码复用:相同的链表代码可以用于多种数据类型,无需为每种类型编写特定的实现。
第12章:STL标准模板库
12.1序列容器
在C++的STL中,序列容器是一类提供了连续内存存储的容器,允许通过索引访问元素,类似于传统的数组。以下是STL中序列容器的分类和简要说明:
-
std::array
:-
固定大小的数组模板,其大小在编译时确定。
-
-
std::vector
:-
动态数组,可以自动调整大小以容纳更多元素。
-
-
std::deque
(双端队列):-
双端队列,允许在两端快速插入和删除元素。
-
-
std::list
:-
双向链表,允许在列表中的任意位置快速插入和删除元素。
-
-
std::forward_list
:-
前向链表,只允许在头部或尾部进行快速插入和删除操作。
-
序列容器的特点:
-
内存连续性:序列容器的元素在内存中连续存储,可以像数组一样通过索引访问元素。
-
随机访问迭代器:序列容器提供随机访问迭代器,允许高效的元素访问。
-
动态大小:除了
std::array
外,其他序列容器的大小可以在运行时动态变化。 -
性能:由于内存连续性,序列容器在访问元素时通常具有很高的性能,但在插入和删除操作时可能会涉及更多的内存操作,特别是靠近容器开始或结束的位置。
-
使用场景:序列容器适用于需要快速随机访问元素的场景,以及在容器大小可能变化时使用。
#include <iostream> #include <vector> int main() { std::vector<int> vec = {1, 2, 3, 4, 5}; // 初始化一个vector // 访问元素 std::cout << "Element at index 2: " << vec[2] << std::endl; // 修改元素 vec[2] = 10; // 遍历vector for (int num : vec) { std::cout << num << " "; } std::cout << std::endl; // 添加元素 vec.push_back(6); // 删除元素 vec.pop_back(); return 0; }
在这个示例中,我们创建了一个 std::vector<int>
类型的向量 vec
,并对其进行了初始化、访问、修改、添加和删除操作。std::vector
是序列容器中最常用的容器之一,因为它提供了动态大小的数组功能和高效的随机访问性能。
12.2结合容器
在C++ STL中,"结合容器"(Associative Containers)是一类特殊的容器,它们通过使用特定的关联结构来存储元素,使得可以快速地通过键值进行查找、插入和删除操作。以下是STL中结合容器的分类和简要说明:
-
std::set
:-
基于红黑树实现的有序集合,不允许有重复的元素。
-
-
std::multiset
:-
也是基于红黑树实现,但允许有重复的元素。
-
-
std::map
:-
基于红黑树实现的有序键值对集合,每个键值对由一个键(key)和一个值(value)组成。
-
-
std::multimap
:-
与
std::map
类似,但允许有重复的键。
-
-
std::unordered_set
:-
基于哈希表实现的无序集合,不允许有重复的元素。
-
-
std::unordered_multiset
:-
基于哈希表实现,允许有重复的元素。
-
-
std::unordered_map
:-
基于哈希表实现的无序键值对集合。
-
-
std::unordered_multimap
:-
与
std::unordered_map
类似,但允许有重复的键。
-
结合容器的特点:
-
基于键的查找:结合容器允许通过键快速查找元素。
-
有序性:
std::set
、std::map
等基于红黑树的容器保持元素的有序性,而std::unordered_set
、std::unordered_map
等基于哈希表的容器则无序。 -
唯一性:
std::set
和std::unordered_set
等容器不允许有重复的元素,而std::multiset
和std::unordered_multiset
允许。 -
性能:结合容器通常提供对元素的快速查找、插入和删除操作,特别是
std::unordered_*
容器在大多数情况下提供接近常数时间的性能。 -
使用场景:结合容器适用于需要快速查找和去重的场景。
#include <iostream> #include <map> int main() { std::map<int, std::string> myMap; // 添加元素 myMap[1] = "one"; myMap[2] = "two"; myMap[3] = "three"; // 遍历map for (const auto& pair : myMap) { std::cout << pair.first << ": " << pair.second << std::endl; } // 查找元素 auto it = myMap.find(2); if (it != myMap.end()) { std::cout << "Found: " << it->second << std::endl; } // 删除元素 myMap.erase(it); return 0; }
这个示例中,我们创建了一个 std::map<int, std::string>
类型的映射 myMap
,并对其进行了添加、遍历、查找和删除操作。std::map
是一个有序的键值对集合,它保持键的排序,并允许通过键快速查找对应的值。
12.3迭代器
在C++ STL中,迭代器(Iterator)是一种抽象概念,用于提供对容器中元素的访问和遍历。迭代器可以被视为一种通用的指针,它允许你通过一致的接口操作不同类型的容器。
迭代器的关键特性:
-
统一接口:迭代器为不同的容器提供了统一的操作接口,如访问元素、遍历容器等。
-
类型安全:迭代器是类型安全的,它们只允许访问其关联容器的元素类型。
-
分类:迭代器分为几种类型,包括输入迭代器(Input Iterators)、输出迭代器(Output Iterators)、正向迭代器(Forward Iterators)、双向迭代器(Bidirectional Iterators)和随机访问迭代器(Random Access Iterators)。
-
操作:迭代器支持的操作包括解引用(*)、成员访问(->)、递增(++)、递减(--)和相等性比较(== 和 !=)。
-
容器支持:大多数STL容器都提供了迭代器,如
std::vector
、std::list
、std::set
等。 -
使用场景:
-
遍历容器:使用迭代器遍历容器中的所有元素。
-
插入元素:使用迭代器指定插入元素的位置。
-
删除元素:使用迭代器指定要删除的元素的位置。
-
-
STL算法:STL中的许多算法都接受迭代器作为参数,如
std::sort
、std::copy
、std::find
等。 -
自定义迭代器:你可以为自定义容器实现迭代器,以提供对容器元素的访问。
-
迭代器失效:在某些操作(如插入或删除元素)后,迭代器可能会失效,需要重新获取有效的迭代器。
-
返回类型:容器的
begin()
和end()
成员函数返回迭代器,分别指向容器的第一个元素和容器末尾的“尾后”位置。
#include <iostream> #include <vector> int main() { std::vector<int> vec = {1, 2, 3, 4, 5}; // 使用迭代器遍历vector for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) { std::cout << *it << " "; } std::cout << std::endl; // 使用范围基 for 循环(C++11引入) for (int num : vec) { std::cout << num << " "; } std::cout << std::endl; return 0; }
在这个示例中,我们使用 begin()
和 end()
成员函数获取指向 vec
开始和结束的迭代器,并使用迭代器遍历 vec
中的所有元素。
第13章:RTTI与异常处理
13.1RTTI(运行时类型识别)
RTTI(Run-Time Type Identification,运行时类型识别)是C++中一种机制,用于在程序运行时获取对象或数据的类型信息。RTTI 包括以下几个关键特性:
-
类型信息:C++ 通过
typeid
对象提供类型信息,它包含一个类型标识符。 -
<typeinfo>
头文件:要使用 RTTI,需要包含<typeinfo>
头文件。 -
std::type_info
类:这是标准库中定义的一个类,存储了类型信息。每个类型都有一个与之关联的std::type_info
对象。 -
typeid
运算符:使用typeid
运算符可以获取对象的类型信息。 -
operator==
和operator!=
:std::type_info
类型的对象可以使用==
和!=
运算符来比较两个对象是否是同一个类型。 -
dynamic_cast
:RTTI 支持dynamic_cast
运算符,它在运行时检查对象的类型,并在需要时进行类型转换。 -
typeid.name()
:std::type_info
对象的name()
成员函数返回一个表示类型的字符串。 -
多态性和虚函数:RTTI 通常与多态性和虚函数一起使用,因为它们允许在运行时识别对象的动态类型。
-
类型安全:RTTI 提供了一种类型安全的方式来处理不同类型的对象。
-
性能开销:使用 RTTI 可能会有一些性能开销,因为它需要在运行时进行类型检查。
#include <iostream> #include <typeinfo> #include <typeindex> #include <type_traits> class Base { public: virtual ~Base() {} }; class Derived : public Base { }; int main() { Derived d; Base* basePtr = &d; // 使用 typeid 检查对象的类型 if (typeid(*basePtr) == typeid(Derived)) { std::cout << "The object is of type Derived." << std::endl; } else { std::cout << "The object is not of type Derived." << std::endl; } // 使用 typeid.name() 获取类型名称 std::cout << "Type name: " << typeid(*basePtr).name() << std::endl; // 使用 dynamic_cast 进行类型转换 Derived* derivedPtr = dynamic_cast<Derived*>(basePtr); if (derivedPtr) { std::cout << "Succeeded in dynamic_cast to Derived." << std::endl; } else { std::cout << "Failed in dynamic_cast to Derived." << std::endl; } return 0; }
13.2异常处理
在C++中,异常处理是一种错误处理机制,它允许程序在发生错误时以一种结构化的方式响应,而不是立即崩溃。异常处理包括以下几个关键概念:
-
异常:异常是程序运行时发生的错误或异常情况的信号。在C++中,异常通常是一个从
std::exception
类派生的类的实例。 -
抛出异常:使用
throw
语句抛出一个异常。这会导致程序流程的立即转移,直到找到相应的异常处理程序。 -
异常处理程序:使用
try
块来标记可能抛出异常的代码区域,并使用catch
块来捕获并处理这些异常。 -
try
块:try
块包含可能会抛出异常的代码。如果在try
块中抛出异常,程序将搜索匹配的catch
块。 -
catch
块:catch
块用于捕获并处理特定类型的异常。可以有多个catch
块来处理不同类型的异常。 -
栈展开:当异常被抛出时,程序会进行栈展开(Stack Unwinding),即调用栈中的函数会依次返回,直到找到匹配的
catch
块。 -
标准异常:C++标准库定义了一些标准的异常类型,如
std::runtime_error
,std::logic_error
,std::out_of_range
等。 -
自定义异常:可以创建自定义异常类,通常从
std::exception
或其子类派生。 -
异常规范:C++11 引入了异常规范(Noexcept),用于指定函数是否不应该抛出异常。
-
std::exception
类:所有标准异常类型都继承自std::exception
类,它定义了异常处理的基本接口,如what()
成员函数。
#include <iostream> #include <exception> #include <stdexcept> //CustomException 是从 std::runtime_error 派生的自定义异常类 class CustomException : public std::runtime_error { public: //这是CustomException 类的一个构造函数用于创建这是CustomException对象,接收一个类型为std::string&的参数,名为message //std::runtime_error(message)是一个初始化列表,它在创建 CustomException 对象时调用基类 std::runtime_error 的构造函数。 CustomException(const std::string& message) : std::runtime_error(message) {} }; void riskyFunction() { //throw关键字抛出一个异常 //创建CustomException 类的一个实例 throw CustomException("Something went wrong!"); } int main() { //try关键字用于开始一个异常处理区域 try { //调用riskyFunction函数,该函数可能抛出一个CustomException类型的异常 riskyFunction(); //catch关键字用于捕获并处理异常 //catch (const CustomException& e):这里的catch专门捕获CustomException类型的异常 //e是异常对象的引用,当一个异常被抛出时,异常对象通常包含有关异常的信息,在catch块中,可以捕获这个异常对象的引用或值,它允许访问异常信息。 } catch (const CustomException& e) { std::cerr << "Caught a custom exception: " << e.what() << std::endl; } catch (const std::exception& e) { std::cerr << "Caught a standard exception: " << e.what() << std::endl; } catch (...) { std::cerr << "Caught an unknown exception." << std::endl; } return 0; }
第14章:文件操作
14.1文件流
在C++中,文件流是标准库提供的一组工具,用于执行文件的输入和输出操作。文件流基于流的概念,允许你以类似于处理标准输入(std::cin
)和标准输出(std::cout
)的方式处理文件。以下是文件流的一些关键概念:
-
文件流对象:
-
std::ifstream
:用于从文件中读取数据。 -
std::ofstream
:用于向文件写入数据。 -
std::fstream
:既可以读取也可以写入数据,取决于打开文件的方式。
-
-
打开文件:
-
使用文件流对象的构造函数或
open
成员函数来打开文件。
-
-
关闭文件:
-
使用
close
成员函数来关闭文件。
-
-
读写操作:
-
使用与标准I/O流相同的操作符
<<
和>>
来执行读写操作。
-
-
文件模式:
-
可以指定不同的模式来打开文件,例如
std::ios::in
、std::ios::out
、std::ios::app
等。
-
-
错误处理:
-
使用
fail
、eof
、bad
等成员函数来检查文件流的状态。
-
-
文件路径:
-
可以提供相对路径或绝对路径来指定要打开的文件。
-
-
二进制模式:
-
使用
std::ios::binary
标志以二进制模式打开文件。
-
-
宽字符文件流:
-
使用
std::wifstream
、std::wofstream
和std::wfstream
处理宽字符数据。
-
-
文件指针:
-
使用
seekg
和seekp
成员函数来移动文件内的读写位置。
#include <iostream> #include <fstream> int main() { // 创建一个用于写入的文件流对象outfile,用于写入文件 //std::ofstream :Output File Stream,输出文件流 std::ofstream outfile; //打开文件"example.txt",以输出模式打开 //std::ios::in ,以输入模式打开,读取文件数据 //std::ios::out 表示输出模式 outfile.open("example.txt", std::ios::out); //判断文件对象outfile是否能成功打开文件 if (!outfile) { std::cerr << "Unable to open file!" << std::endl; return 1; } // 写入数据到文件 outfile << "Hello, World!" << std::endl; // 关闭文件 outfile.close(); // 创建一个用于读取的文件流对象 //std::ifstream :Input File Stream,输入文件流 std::ifstream infile; //表示输入模式 : std::ios::in infile.open("example.txt", std::ios::in); if (!infile) { std::cerr << "Unable to open file!" << std::endl; return 1; } std::string line; // 读取文件中的数据 std::getline(infile, line); std::cout << "Read from file: " << line << std::endl; // 关闭文件 infile.close(); return 0; }
-
在这个示例中,我们首先创建了一个std::ofstream
对象来写入文本到example.txt
文件中。然后,我们使用std::ifstream
对象读取文件中的数据。通过检查文件流对象的状态,我们可以确定文件是否成功打开。
14.2文件打开与读写
文件流类
-
std::ofstream
:用于文件输出。 -
std::ifstream
:用于文件输入。 -
std::fstream
:可以用于文件的输入和输出。
文件打开模式
-
std::ios::in
:以输入模式打开文件,用于读取。 -
std::ios::out
:以输出模式打开文件,用于写入。 -
std::ios::app
:以追加模式打开文件,写入时数据会被追加到文件末尾。 -
std::ios::binary
:以二进制模式打开文件,用于读写二进制文件。
文件打开
-
使用文件流对象的
open
方法或构造函数来打开文件。 -
检查文件是否成功打开,通常使用
is_open()
方法。
文件读写
-
写入:使用插入运算符
<<
将数据写入文件。 -
读取:使用提取运算符
>>
或getline
函数从文件中读取数据。
文件关闭
-
使用文件流对象的
close
方法关闭文件。 -
确保数据被写入并释放系统资源。
错误处理
-
检查文件操作是否成功,处理可能发生的异常或错误。
文件指针和位置
-
使用
seekg
和seekp
函数移动文件读取或写入位置。
缓冲区
-
了解文件流的缓冲机制,使用
flush
或endl
刷新缓冲区。
示例知识结构
-
文件流类选择:
-
根据需要选择
std::ofstream
、std::ifstream
或std::fstream
。
-
-
模式标志组合:
-
根据需求组合不同的模式标志,如
std::ios::out | std::ios::app
。
-
-
文件打开与检查:
-
使用
open()
方法打开文件,并用is_open()
检查成功与否。
-
-
读写操作:
-
写入操作:
outfile << data;
-
读取操作:
infile >> data;
或std::getline(infile, data);
-
-
流操纵算子:
-
使用
std::endl
插入换行并刷新缓冲区。
-
-
错误处理:
-
使用条件判断和异常捕获处理潜在错误。
-
-
文件关闭:
-
使用
close()
方法关闭文件,确保数据完整性。
-
-
文件指针操作:
-
使用
seekg()
和seekp()
进行文件定位。
-
-
宽字符和二进制文件:
-
使用
std::wfstream
处理宽字符数据,使用std::ios::binary
处理二进制文件。
-
14.3文件指针移动操作
文件指针移动操作是指在文件流中改变文件指针的位置,从而改变文件读写操作的当前位置。在C++中,文件指针移动操作主要用于以下两个目的:
-
跳过数据:在读取文件时,可以跳过某些数据,或者在写入时跳过某些位置。
-
重新定位:在读写操作中重新定位文件指针,以便在文件中的不同位置进行操作。
1. seekg()
和 seekp()
-
seekg()
:用于移动输入文件指针,使其指向新的位置。 -
seekp()
:用于移动输出文件指针,同样使其指向新的位置。
这两个函数的原型如下:
istream& seekg(streampos pos); // 对于输入操作 ostream& seekp(streampos pos); // 对于输出操作
-
pos
参数是一个streampos
类型,表示从文件流的起始位置开始移动的位置。
2. tellg()
和 tellp()
-
tellg()
:返回当前输入文件指针的位置。 -
tellp()
:返回当前输出文件指针的位置。
这两个函数的原型如下:
streampos tellg(); // 返回输入文件指针的当前位置 streampos tellp(); // 返回输出文件指针的当前位置
3. 移动方式
-
std::ios::beg
:从文件开始处定位。 -
std::ios::cur
:从当前文件指针位置定位。 -
std::ios::end
:从文件结束处定位。
#include <iostream> #include <fstream> int main() { std::fstream file("example.txt", std::ios::in | std::ios::out); if (!file) { std::cerr << "Unable to open file!" << std::endl; return 1; } // 移动到文件开头 file.seekg(0, std::ios::beg); // 读取一些数据 char buffer[100]; file.read(buffer, sizeof(buffer)); // 移动到文件末尾并写入数据 file.seekp(0, std::ios::end); file.write("New data", 8); // 获取当前文件指针位置 std::streampos position = file.tellg(); // 对于输入操作 std::streampos position_out = file.tellp(); // 对于输出操作 file.close(); return 0; }
14.4文件和流的关联和分离
文件和流的关联:
-
关联过程:当你使用文件流(如
std::ifstream
、std::ofstream
、std::fstream
)打开一个文件时,你将文件和流对象关联起来。这个过程称为"打开文件"。 -
流对象:流对象充当文件和程序之间的中介,提供了一系列操作文件的方法,如读写、定位文件指针等。
-
文件路径:在关联过程中,你需要指定文件的路径,以便流对象知道要操作的文件位置。
-
打开模式:关联时,你还需要指定打开文件的模式,如只读、只写、追加等。
-
文件指针:一旦文件和流对象关联,流对象内部的文件指针将被初始化,通常指向文件的开始位置。
-
数据交换:关联后,程序就可以通过流对象读写文件中的数据,数据通过流对象和文件指针在程序和文件之间交换。
文件和流的分离:
-
分离过程:当文件操作完成后,或者需要关闭文件时,流对象与文件的关联被解除,这个过程称为"关闭文件"。
-
关闭操作:通过调用流对象的
close()
方法,可以关闭文件,这会导致文件和流对象的分离。 -
资源释放:分离时,系统会释放与文件关联的资源,如文件描述符等。
-
数据刷新:在分离前,流对象通常会刷新其缓冲区,确保所有待写入的数据都被写入到文件中。
-
文件指针重置:分离后,文件指针的位置可能会被重置或变得无效。
-
对象销毁:当流对象被销毁时,如超出作用域或被显式删除,它与文件的关联也会自动分离。
-
重新打开:分离后,如果需要再次操作文件,可以重新调用流对象的
open()
方法来建立新的关联。
std::ifstream infile; infile.open("example.txt", std::ios::in); // 关联文件和流 if (infile.is_open()) { // 执行文件操作 infile.close(); // 分离文件和流 }
14.5删除文件
在C++中,可以使用标准库中的文件流功能来删除文件,但这通常是通过操作系统级别的API来实现的。C++17标准之前,标准库并没有直接提供删除文件的函数。不过,可以使用 <filesystem>
头文件中的功能,它在C++17中被引入。
#include <iostream> #include <filesystem> int main() { std::filesystem::path filePath("example.txt"); // 设置要删除的文件路径 try { if (std::filesystem::remove(filePath) != 0) { // 如果remove函数返回非0值,表示删除失败 std::cerr << "Failed to delete the file: " << filePath << std::endl; } else { // 删除成功 std::cout << "File deleted successfully: " << filePath << std::endl; } } catch (const std::filesystem::filesystem_error& e) { // 捕获并处理可能发生的文件系统错误 std::cerr << "Filesystem error: " << e.what() << std::endl; } return 0; }标签:std,函数,int,成员,Typora,笔记,C++,模板,指针 From: https://blog.csdn.net/weixin_55673851/article/details/140100450