C++基础学习笔记
第一章 认识C++
1.1 命名空间
1.1.1 命名空间的基本格式
- 命名空间是一个由用户自己定义的作用域,在不同作用域中定义相同变量,不会冲突。
- 命名空间中可以存放以下类型,这些定义/声明在结构体中的内容成为实体
- 变量
- 常量
- 函数(可以是定义或声明)
- 结构体
- 类
- 模板
- 命名空间(可以嵌套定义)
namespace wd{
int number = 0;
struct Foo
{
int val;
}
void display();
}//end of namespace wd
1.1.2 命名空间的使用方式
-
命名空间一共有三种使用方式
-
using编译指令
-
将该空间中的全部实体一次性引入到程序中
using namespace std;
-
-
作用域限定符
-
每次要使用某个名称空间中的实体时,都直接加上"::"
namespace wd { int number = 10; void display() { //cout,endl都是std空间中的实体,所以都加上'std::'命名空间 std::cout << "wd::display()" << std::endl; } }
-
-
using声明机制
- using声明机制的作用域是从using语句开始,到using所在的作用域结束。
- 在同一作用域内用using声明的不同的命名空间的成员不能有同名的成员,否则会发生重定义。(同一作用域内不可重名)
//同一作用域内不同空间不可重名 #include <iostream> //using声明机制 using std::cout; using std::endl; //作用域限定符 namespace wd { int number = 10; void display() { cout << "wd::display()" << endl; } }//end of namespace wd using wd::number; using wd::display; int main(void) { cout << "wd::number = " << number << endl; wd::display(); }
-
1.1.3 匿名命名空间
- 匿名空间可以不定义名字,该空间中的实体,其他文件无法引用,它只能在本文件的作用域内有效。
- 在匿名空间中创建的全局变量,具有全局生存周期,却只能被本空间内的函数访问,是static变量的有效替代手段。
namespace{
//其中vall只可以被本命名空间内的函数访问,是全局变量。
//外部空间不可以访问
int vall = 10;
void func();
}
1.1.4 命名空间的嵌套及覆盖
#include <iostream>
using namespace std;
int number = 1;
namespace wd
{
int number = 10;
namespace luo
{
int number = 100;
void display()
{
cout << "wd::luo::display()" << endl;
}
}//end of namespace wd
void display(int number)
{
cout << "形参number = " << number << endl;
cout << "wd命名空间中的number = " << wd::number << endl;
cout << "luo命名空间中的number = " << wd::luo::number << endl;
}
}//end of namespace wd
int main(void)
{
using wd::display;
display(number);
return 0;
}
- 只有在使用命名空间的时候,参数才是命名空间内部得参数
- 嵌套命名空间得时候,想使用嵌套命名空间中得参数,要嵌套使用::
1.1.5 命名空间的使用方法
-
在已命名的空间中定义变量,而不是直接定义外部全局变量或者静态变量
-
如果开发了一个函数库或者类库,提前将其放在一个名称空间中
-
对于using声明,将其作用域设置为局部而不是全局
-
不要在头文件中使用using编译指令,这样使得可用名称变得模糊,容易出现二义性。
-
包含头文件的顺序可能会影响程序的行为,如果非要使用using编译指令,建议放在所有#include预编译指令后。
比如
#include <iostream> using namespace std; //std命名空间就是头文件iostream中函数的声明位置
1.2 const关键字的用法
-
const 与宏定义的区别
- 编译器处理方式不同
- 宏定义是在预处理阶段展开,做字符串的替换
- const是常量在编译时候分配空间
- 类型和安全检查不同
- 宏定义没有类型,不做任何类型检查
- const常量具有具体的类型,在编译器会执行类型检查
- const常量必须要进行初始化
- 编译器处理方式不同
-
常量指针与指针常量
-
const pointer
指针可以改变指向,不可以通过指针修改常量的值const int *p1 = # int const *p1 = # //const直接修饰*p1表示*p1不可变,即变量的值不可变
-
pointer to const
指针不可以改变指向,可以通过指针改变变量
int* const p3 = &number; //const直接修饰p3,表示p3的值不可以改变,即指针的地址不可以改变
-
1.3 new/delete表达式
1.3.1 new/delete的作用
- new/delete在c++中用来开辟和回收堆空间
- malloc/free在c语言中用来开辟和回收堆空间
虚拟内存空间分布
低 ----------------------------------------------------------------------------------------------> 高
代码段 存储代码指令 | 数据段 存储全局静态变量 | 堆空间 动态内存管理,由低->高生长 | 栈空间 存储局部变量,由低<-高生长 | 内核区域用户态->内核态 |
---|
1.3.2 用new/delete开辟空间
//开辟一个元素空间
int * p = new int(1);
//释放一个元素的空间
delete p;
//开辟一个数组空间
int * p = new int[10]();
//释放一个数组的空间
delete []p;
- new/delete自动初始化,自动分配空间大小;malloc不可以
1.4 引用
1.4.1 引用的概念
-
变量名是一段连续内存空间的别名,引用就是把这段连续的内存空间再取一个别名
void test0{ int a = 1; int & ref1 = a; //ref2是一个不完整的引用声明,它没有指向任何变量 //一个完整的引用一定要进行初始化不然会报错 int & ref2; }
-
引用要注意的点
- &不再是取地址符号,而是引用符号。
- 引用类型需要和被引用的值的类型保持一致
- 声明引用时候一定要初始化
- 一旦绑定到某个变量之后,就不会再改变其指向
1.4.2 引用的本质
引用就是被限制的指针,占据一个指针的内存,存放一个地址,一旦被绑定就不可以再改变
1.4.3 引用作为函数参数
- 通过形参改变改变实参的值
- 指针:不好操作,比较复杂
- 引用:有更好的可读性和实际意义
- 值传递:副本进行传递,不划算
1.4.4 引用作为函数的返回值
当以引用作为函数的返回值的时候,返回的变量其生命周期一定大于函数的生命周期,函数执行完毕时,返回的变量还存在。
int temp;
int & func1(){
temp = 100;
return temp;
}
-
返回的类型为引用要注意的点
-
不能返回局部变量的引用
-
不能在函数内部返回new分配的堆空间变量的引用。
如果返回的引用只是作为一个临时变量出现,而没有被赋予一个实际的变量该引用所在的空间就无法释放,会造成内存泄漏。
-
//number是一个局部变量,func3结束之后就被销毁了,返回错误
int & func3(){
int number = 1;
return number;
}
//在函数内部申请的地址,在函数结束之后就释放了,但是却返回了引用(地址),导致内存泄漏
int & fun4(){
int * pint = new int(1);
return *pint;
}
- 引用总结
- 引用主要是用于参数传递时,解决副本,指针操作可读性差的问题
- 通过对const的使用,保证了引用传递的安全性。
第二章 类与对象基础
2.1 面向对象的思想
面向对象的三大基本特征是:
- 封装:隐藏内部实现
- 继承:复用现有代码
- 多态:改写对象行为
2.2 类的定义
2.2.1 类的概念速览
类的定义:
- 数据成员: 相当于现实世界中的属性
- 成员函数: 对数据的操作
- 注意:
- 定义类名遵循大驼峰规则
- 定义成员函数名遵循小驼峰规则
- 定义数据线成员名在前面加上下划线
class MyClass{//类的定义
//……
void myFunc(){} //成员函数
int _a; //数据成员
};//一定要有分号
//类也可以先声明,后完成定义
class MyClass2;//类的声明
class MyClass2{//类的定义
//……
};//分号不能省略
访问修饰符:
- public: 公有的访问权限,在类外可以通过对象直接访问公有成员。
- protected: 保护的访问权限,派生类中可以访问,在类外不能通过对象直接访问。
- private: 私有的访问权限,在本类之外不能访问,比较敏感的数据设为private。
- 注意:
- class定义中如果在成员定义(或声明)之前没有任何访问修饰符,其默认的访问权限为私有。
struct和class的对比:
C语言中的struct可以封装数据,但是不能隐藏数据,而且成员不能是函数。
C++中中的struct对C中的struct做了拓展,基本等同于class,默认访问权限是public。
class的默认访问权限是private。
typedef struct{
int number;
char name[25];
int score;
void (*p)();
//void print();//error
}
成员函数的定义:
成员函数定义的形式: 成员函数可以在类内部完成定义,也可以在类内部只进行声明,在类外部完成定义。
多文件联合编译时可能出现的错误: 如果在头文件中对函数进行定义,头文件内容在每个源文件都会复制一份,每个源文件都会生成一份目标文件,可能会导致在链接阶段出现相同函数定义的情况,导致重定义错误。
class Computer {
public:
//成员函数
void setBrand(const char * brand);//设置品牌
void setPrice(float price);//设置价格
void print();//打印信息
private:
//数据成员
char _brand[20];
float _price;
};
void Computer::setBrand(const char * brand){
strcpy(_brand, brand);
}
void Computer::setPrice(float price){
_price = price;
}
解决多文件联合编译错误的方法:
-
解决方法1:在成员函数的定义前加上inline关键字,说明类内部定义的成员函数就是inline函数
inline void Computer::setBrand(const char * brand){ strcpy(_brand, brand); } inline void Computer::setPrice(float price){ _price = price; }
-
解决方法2:将成员函数的定义放在类的内部(和方法一本质上是一样的效果)
-
解决方法3:函数声明放在头文件,函数定义放在实现文件中(一个.c一个.h)。
最常用的方法就是方法3
构造函数(如何创建一个对象):
构造函数: 和类同名的函数称为构造函数,构造函数可以重载,如果没有构造函数自动用无参构造函数。
初始化列表: 利用初始化列表对对象的数据成员完成初始化,数据成员初始化的顺序与其声明的顺序保持一致,与他们在初始化列表中的顺序无关(但初始化列表一般保持与数据成员声明的顺序一致)
class Point {
public:
Point(){}
Point(int ix,int iy = 0);
//可以在声明中设定参数的默认值
private:
//c++11之后也可以初始化数据成员,但是一般情况下还是在初始化列表中对数据成员初始化
int _ix;
int _iy;
};
inline void Point::Point(int ix, int iy)
:_ix(ix)
,_iy(iy)
{
cout << "Point(int,int)" << endl;
}
指针数据成员初始化:
类的数据成员中有指针时,意味着创建该类的对象时要进行指针成员的初始化,需要申请堆空间。
class Computer {
public:
Computer(const char * brand, double price)
//+1加的是\0
: _brand(new char[strlen(brand) + 1]())//这里新new的内存要交给析构函数进行回收
, _price(price)
{
strcpy(_brand,brand);
}
private:
char * _brand;
double _price;
};
void test0(){
Computer pc("Apple",12000);
}
对象所占空间大小:内存对齐机制,为什么要进行内存对齐:
- 1.平台原因: 不是所有的而硬件平台都能访问任意地址上的任意数据的。
- 2.性能原因: 64位系统默认以8个字节块大小进行读取,对齐可以防止系统读取一块儿数据的时候多次访问内存。
- 规则:按照类中占空间最大的数据成员大小的倍数对齐。
- 注意:如果数据成员中有数组类型,会按照除数组以外的其他数据成员中最大的那一个的倍数 对齐。
class C{
int _c1;
int _c2;
double _c3;
};
//sizeof(C) = 16
class D{
int _d1;
double _d2;
int _d3;
};
//sizeof(D) = 24
析构函数(如何销毁一个对象):
析构函数的概念:
- 定义:对象在销毁时调用的函数
- 作用:清理对象的数据成员申请的资源(堆空间)
- 形式:
- 没有返回值,即使是void也没有。
- 没有参数。
- 函数名与类名相同,在类名之前要加上一个(不加就是构造函数)
- 性质:
- 析构函数不可以重载(构造函数可以)
- 析构函数默认情况下,系统会自动提供一个
- 当对象被销毁时,会自动调用析构函数
- 不建议手动调用析构函数,因为容易导致各种问题,应该让析构函数自动被调用。
自定义析构函数:
-
什么时候需要自定义析构函数: 当数据成员中有指针时,创建一个对象,会申请堆空间,销毁对象时默认析构不够用了 (造成内存泄漏),此时就需要我们自定义析构函数。在析构函数中定义堆空间上内存回 收的机制,就不会发生内存泄漏。
class Computer { public: Computer(const char * brand, double price) : _brand(new char[strlen(brand) + 1]()) , _price(price) {} ~Computer() { if(_brand){ delete [] _brand; _brand = nullptr//设为空指针,安全回收 } cout << "~Computer()" << endl; } private: char * _brand; double _price; };
构造函数和析构函数的调用时机(重点):
-
全局定义的对象: 在主函数main接收程序控制权之前,就调用构造函数创建全局对象,在整个程序结束时,自动调用全局对象的析构函数。
-
局部定义的对象: 每当程序流程到达该对象的定义处就调用构造函数,在程序离开局部对象的作用域时调用对象的析构函数。
-
static定义的静态对象: 当程序流程到达该对象定义处调用构造函数,在整个程序结束时调用析构函数。
-
new创建的堆对象: 每当创建该对象时调用构造函数,在使用delete删除该对象时,调用析构函数。
Computer *p1 = new Computer("Lenovo",6500); P1->print(); delete P1; P1 = nullptr;
拷贝构造函数(如何复制一个对象):
拷贝构造函数的定义: 用一个变量初始化另一个变量
拷贝构造函数的形式: 类名(const 类名 &)
- 拷贝构造函数也是一个构造函数
- 该函数用一个已经存在的同类型的对象,来初始化新的对象,即对对象本身进行复制
拷贝构造函数的形式探究:
-
拷贝构造函数是否可以去掉引用符号:
不可以。
如果拷贝函数的参数中去掉引用符号,进行拷贝时调用拷贝构造函数的过程中会发生“实参和形参都是对象,用实参初始化形参”(拷贝构造第二种调用时机),会再一次调用拷贝构 造函数。形成递归调用,直到栈溢出,导致程序崩溃。
-
拷贝构造函数是否可以去掉const:
在复制临时对象内容的时候会报错:
Computer pc3 = Computer("ASUS",5000);
- 加const的第一个用意: 为了确保右操作数的数据成员不被改变。
- 加const的第二个用意: 为了能够复制临时对象的内容,因为非const引用不能绑定临时变 量(右值)。
拷贝构造函数的性质:
-
使用默认的拷贝构造函数,会进行浅拷贝,即两个指针指向同一片内存
Point(const Point & rhs) : _ix(rhs._ix) , _iy(rhs._iy) {}
-
当要进行拷贝构造的类的数据成员有指针(要申请堆空间)的时候,要将拷贝构造显示写出,采用深拷贝的方式,先申请空间,再复制内容。
Computer::Computer(const Computer & rhs) : _brand(new char[strlen(rhs._brand) + 1]()) , _price(rhs._price) { strcpy(_brand, rhs._brand); }
拷贝构造函数的调用时机(重点):
-
当使用一个已经存在的对象初始化另一个同类型的新对象时。
-
当函数参数(实参和形参的类型都是对象),形参与实参结合时(实参初始化形参)。
为了避免这次不必要的拷贝,可以使用引用作为参数。
//调用拷贝构造 void func(Comptuer rhs){ rhs.print(); } //引用避免调用拷贝构造 void func(Computer & rhs){ rhs.print(); } void test1(){ Computer pc("apple",2000); func(pc); }
-
当函数的返回值是对象,执行return语句时(编译器有优化)。
为了避免这次多余的拷贝,可以使用引用作为返回值,但一定要确保返回值的生命周期大于函数的生命周期。
Computer pc3("Acer",5400); //调用拷贝构造函数 Computer func2(){ return pc3; } //直接引用,避免调用拷贝构造函数 Computer & func2(){ return pc3; }
赋值运算符函数:
赋值运算符函数的执行时机: 在执行 pt1 = pt2; 该语句时, pt1 与 pt2 都存在,所以不存在对象的构造,这要与 Point pt2 =pt1; 语句区分开,这是不同的。
Point pt1(1, 2), pt2(3, 4);
pt1 = pt2;//赋值操作
赋值运算符函数的形式: 类名& operator=(const 类名 &)
Point & Point::operator=(const Point & rhs)
{
_ix = rhs._ix;
_iy = rhs._iy;
//返回本对象的this指针
return *this;
}
赋值运算符函数的形式探究:
-
赋值运算符函数的返回必须是一个引用吗?
可以不是但是会造成一次多余的拷贝,增加不必要的开销。
-
赋值操作符函数的返回类型可以是void吗?
可以是但是无法处理连续赋值
-
赋值操作符函数的参数一定要是引用吗?
可以不是但是会造成一次多余的拷贝,增加不必要的开销
-
赋值操作符函数的参数必须是一个const引用吗?
无法避免在赋值运算符函数中修改右操作的内容,不合里,而且当右操作数为临时对象的时候,不是const的引用会报错。
赋值运算符函数的定义:
-
如果对象的指针数据成员申请了堆空间,默认的赋值运算符函数就不够用了,是浅拷贝。
Computer & operator=(const Computer & rhs){ this->_brand = rhs._brand; this->_price = rhs._price; return *this; }
-
直接进行深拷贝是否可行?
不可行,会发生内存泄漏。
因为创建对象的时候就已经申请了空间,只有先把创建对象时候申请的空间释放,然后再申请空间才不会造成内存泄漏。
Computer & operator=(const Computer & rhs){ if(this != &rhs){ delete [] _brand; _brand = new char[strlen(rhs._brand)](); strcpy(_brand,rhs._brand); _price = rhs._price; } return *this; }
-
总结——四步走(重点):
- 考虑自复制问题
- 回收左操作数原本申请的堆空间
- 深拷贝(以及其他的数据成员的复制)
- 返回*this
this指针:
this指针的本质:this指针指向本对象,this是一个隐藏的指针,可以在类的成员函数中使用,它可以用来指向调用对象。
this指针存在那里: 寄存器——编译器在生成程序时加入了获取对象首地址的相关代码,将获取的首地址存放在了寄存器 中,这就是this指针。
this指针的生命周期: 开始于成员函数的执行开始,结束于成员 函数的执行结束。如果成员函数是通过一个已经销毁或未初始化的对象调用的,this指针将是悬挂的,它的 使用将会是未定义行为。
Point & operator=(const Point & rhs){
this->_ix = rhs._ix;
this->_iy = rhs._iy;
cout << "Point & operator=(const Point &)" << endl;
return *this;
}
成员函数中可以加上this指针,展示本对象通过this指针找到本对象的数据成员。但是不要 在参数列表中显式加上this指针,因为编译器一定会在参数列表的第一位加上this指针,如果显式再给一个,参数数量就不对了。
三合成原则
拷贝构造函数、赋值运算符函数、析构函数,如果需要手动定义其中的一个,那么另外两 个也需要手动定义。
2.2.2 特殊的数据成员与成员函数
特殊的数据成员
-
常量数据成员: const数据成员需在初始化列表中进行初始化,在程序中无法堆const数据成员进行修改。
class Point { public: Point(int ix, int iy) : _ix(ix) , _iy(iy) {} private: const int _ix; const int _iy; };
-
引用数据成员: 引用数据成员在初始化列表中进行初始化,C++11之后允许在声明进行引用的初始化。
class Point { public: Point(int ix, int iy) : _ix(ix) , _iy(iy) , _iz(_ix) {} private: const int _ix; const int _iy; int & _iz; };
如果在构造函数中再接收一个参数的话,不可以用这个参数成员进行初始化。
//error code class Point { public: Point(int ix, int iy,int iz) : _ix(ix) , _iy(iy) , _iz(iz)//这里值传递的时候会发生赋值,引用就像绑定了一个临时变量,临时变量的声明周期只有当前行 {} private: const int _ix; const int _iy; int & _iz; };
-
对象数据成员: 一个类对象会作为另一个类对象的数据成员被使用,对象成员必须在初始化列表中进行初始化。
注意:-
不能在声明对象成员时就去创建
-
初始化列表中写的是需要被初始化的对象成员的名称,而不是对象成员的类名。
class Line { public: Line(int x1, int y1, int x2, int y2) : _pt1(x1, y1) , _pt2(x2, y2) { cout << "Line(int,int,int,int)" << endl; } private: Point _pt1;//Point(x1,y1); Point _pt2;//Point(x2,y2); };
-
在此函数中创建一个Line的对象,首先会调用Line的构造函数,在此过程中调用Point的构造函数。
-
在Line对象销毁的时候会先调用Line的析构函数,析构函数执行完之后,再调用Point的析构函数。
-
-
静态数据成员: C++ 允许使用 static (静态存储)修饰数据成员,这样的成员在编译时就被创建并初始化的(与之相比,对象是在运行时被创建的),静态数据成员存储在全局/静态区,并不占据对象的存储空间。静态数据成员被整个类的所有对象共享。
class Computer { public: //... private: char * _brand; double _price; //数据成员的类型前面加上static关键字 //表示这是一个static数据成员(共享) static double _totalPrice; }; double Computer::_totalPrice = 0;
静态数据成员规则:
- private的静态数据成员无法在类之外直接访问(显然)。
- 对于静态数据成员的初始化,必须放在类外(一般紧接着类的定义,这是规则1的特殊情况)。
- 静态数据成员初始化时不能在数据类型前面加static,在数据成员名前面要加上类名+作用域限定符。
- 如果有多条静态数据成员,那么它们的初始化顺序需要与声明顺序一致(规范)
-
静态成员函数: 在某一个成员函数的前面加上static关键字,这个函数就是静态成员函数。
静态成员函数的特点:
- 静态成员函数不依赖于某一个对象。
- 静态成员函数可以通过对象调用,但更常见的方式是通过类名加上作用域限定符调用。
- 静态成员函数没有this指针。
- 静态成员函数无法直接访问非静态的成员,只能访问静态数据成员或调用静态成员函数(因为没有this指针)。(但是非静态的成员函数可以访问静态成员)
- 静态成员函数不能是构造函数/析构函数/赋值运算符函数/拷贝构造(因为这四个函数都会访问所有的数据成员,而static成员函数没有this指针)
class Computer { public: Computer(const char * brand, double price) : _brand(new char[strcpy(brand) + 1]()) , _price(price) { _totalPrice += _price; } //... //静态成员函数 static void printTotalPrice() { cout << "totalPrice:" << _totalPrice << endl; cout << _price << endl;//error,不可以直接访问非静态的数据成员,因为没有this指针。 } private: char * _brand; double _price; static double _totalPrice; }; double Computer::_totalPrice = 0;
如何完成Computer类的总价计算逻辑:除了构造函数之外,对Computer的复制构造函数,赋值运算符重载函数,析构函数都要进行更改
-
const成员函数: 在成员函数的参数列表之后,函数执行体之前加上const关键字,这个函数就是const成员函数。
形式: void func() const {}class Computer{ public: //... void print const{ cout << "brand:" << _brand << endl; cout << "price:" << _price << endl; } //... };
const成员函数的特点:
- const成员函数中,不能修改对象的数据成员。
- 当编译器发现该函数是const成员函数时,会自动将this指针设置为双重const限定的指针。双重const指针的目的是限制this指针不可以修改数据成员的值。
//原本的this指针类型 Point * const this //const成员函数的this指针 const Point * const this //前面一个const的作用是不能修改Point对象 //如果Point对象由int _ix / int _iy / int * _pint //对于_pint,const属性是施加在指针层面,也就是说不能修改这个指针,代表着不能修改这个指针的指向,但是并不能限制它修改指向的值 //如果指针数据成员 const int * _pint的效果就是可以修改指向,不能修改指向的值
-
const对象: 一般来说,能作用于 const 对象的成员函数除了构造函数和析构函数,就只有 const 成员函数了。因为 const 对象只能被创建、撤销和只读访问,写操作是不允许的。
const对象与const成员函数的规则:- 当类中有const成员函数和非const成员函数重载时,const对象会调用const成员函数,非const对象会调用非const成员函数;
- 当类中只有一个const成员函数时,无论const对象还是非const对象都可以调用这个版本;
- 当类中只有一个非const成员函数时,const对象就不能调用非const版本。
- 总结: 如果一个成员函数中确定不会修改数据成员,就把它定义为const成员函数。
- 思考: 一个类中可以有参数形式“完全相同”的两个成员函数(const版本与非const版本),既然没有报错重定义,那么它们必然是构成了重载,为什么它们能构成重载呢?
—— 参数(this指针)是不同的。
对象的构造
-
指向对象的指针:
Point pt(1, 2); Point * p1 = nullptr; Point * p2 = &pt; Point * p3 = new Point(3, 4);
-
对象数组:
Point pts[2] = {Point(1, 2), Point(3, 4)}; Point pts[] = {Point(1, 2), Point(3, 4)}; Point pts[5] = {Point(1, 2), Point(3, 4)}; //或者 Point pts[2] = {{1, 2}, {3, 4}}; Point pts[] = {{1, 2}, {3, 4}}; Point pts[5] = {{1, 2}, {3, 4}};
-
堆对象: 和把一个简单变量创建在动态存储区一样,可以用 new 和 delete 表达式为对象分配动态存储区,在拷贝构造函数一节中已经介绍了为类内的指针成员分配动态内存的相关范例,这里主要讨论如何为对象和对象数组动态分配内存。
void test() { Point * pt1 = new Point(11, 12); pt1->print(); delete pt1; pt1 = nullptr; Point * pt2 = new Point[5]();//注意 pt2->print(); (pt2 + 1)->print(); delete [] pt2; pt2 = nullptr; }
2.3 单例模式
2.3.1 单例模式的概念
单例模式的概念: 单例模式是23种常用设计模式中最简单的设计模式之一,它提供了一种创建对象的方式,确保只有单个对象被创建。这个设计模式主要目的是想在整个系统中只能出现类的一个实例,即一个类只有一个对象。
2.3.2 将单例模式创建在静态区
步骤:
- 将构造函数私有。
- 通过静态成员函数getInstance创建局部静态对象,确保对象的生命周期和唯一性;
- getInstance的返回值设为引用,避免复制;
代码:
class Point
{
public:
//getlnstance是静态成员函数,因此在整个程序的声明周期内只被调用一次
static Point & getInstance(){
static Point pt(1,2);
return pt;
}
void print() const{
cout << "(" << this->_ix
<< "," << this->_iy
<< ")" << endl;
}
private:
//构造函数私有,只能通过函数显示调用
Point(int x,int y)
: _ix(x)
, _iy(y)
{
cout << "Point(int,int)" << endl;
}
private:
int _ix;
int _iy;
};
void test0(){
Point & pt = Point::getInstance();
pt.print();
Point & pt2 = Point::getInstance();
pt2.print();
cout << &pt << endl;
cout << &pt2 << endl;
}
2.3.3 将单例对象创建在堆区
步骤:
- 构造函数私有。
- 通过静态成员函数getInstance创建堆上的对象,返回Point*类型的指针。
- 通过静态成员函数完成堆对象的回收。
2.3.4 单例对象的数据成员申请堆空间
2.3.5 单例模式的应用场景
- 有频繁实例化然后销毁的情况,也就是频繁的 new 对象,可以考虑单例模式。
- 创建对象时耗时过多或者耗资源过多,但又经常用到的对象;
- 当某个资源需要在整个程序中只有一个实例时。
- 当需要读取和管理程序配置文件时,可以使用单例模式确保只有一个实例来管理配置文件的读取和写入操作(配置文件管理)。
- 在多线程编程中,线程池是一种常见的设计模式。使用单例模式可以确保只有一个线程池实例,方便管理和控制线程的创建和销毁;
- GUI应用程序中的全局状态管理:在GUI应用程序中,可能需要管理一些全局状态,例如用户信息、应用程序配置等。使用单例模式可以确保全局状态的唯一性和一致性。
2.4 C++字符串
C风格的字符串以\0
结尾,C++中c风格的字符串的表示:1. const char *
2. " "
2.4.1 C++风格的字符串
C++ 提供了std::string
类用于字符串的处理.
与
C 风格字符串相比,string
不必担心内存是否足够、字符串长度,结尾的空白符等等。
std::string
标准库提供的一个自定义类类型basic_string
,string
类本质上其实是basic_string
类模板关于 char
型的实例化。 使用起来不需要关心内存直接使用即可。
2.4.2 string的构造
string
对象的常用构造:
string();//默认构造函数,生成一个空字符串
string(const char * rhs);//通过c风格字符串构造一个string对象
string(const char * rhs, size_type count);//通过rhs的前count个字符构造一个string对象
string(const string & rhs);//拷贝构造函数
string(size_type count, char ch);//生成一个string对象,该对象包含count个ch字符
string(InputIt first, InputIt last);//以区间[first, last)内的字符创建一个string对象
用拼接的方式构造string
:
//采取拼接的方式创建字符串
//可以拼接string、字符、C风格字符串
string str3 = str1 + str2;
string str4 = str2 + ',' + str3;
string str5 = str2 + ",world!";
2.4.3 string的常用操作
const CharT* data() const;
const CHarT* c_str() const; //C++字符串转为C字符串
bool empty() const; //判空
size_type size() const;//获取字符数
size_type length() const;
void push_back(CharT ch); //字符串结尾追加字符
//在字符串的末尾添加内容,返回修改后的字符串
basic_string& append(size_type count, CharT ch); //添加count个字符
basic_string& append(const basic_string& str); //添加字符串
basic_string & append(const basic_string& str, //从原字符串的pos位置,添加字符串的count个字符
size_type pos,size_type count);
basic_string& append(const charT* s); //添加C风格字符串
//查找子串
size_type find( const basic_string& str, size_type pos = 0 ) const; //从C++字符串的pos位开始查找C++字符串
size_type find( CharT ch, size_type pos = 0 ) const; //从C++字符串的pos位开始查找字符ch
size_type find( const CharT* s, size_type pos, size_type count ) const;
//从C++字符串的pos位开始,去查找C字符串的前count个字符
注意: 两个basic_string字符串比较,可以直接使用 == 等符号进行判断。
原理: basic_string对 == 运算符进行了默认重载。
//非成员函数
bool operator==(const string & lhs, const string & rhs);
bool operator!=(const string & lhs, const string & rhs);
bool operator>(const string & lhs, const string & rhs);
bool operator<(const string & lhs, const string & rhs);
bool operator>=(const string & lhs, const string & rhs);
bool operator<=(const string & lhs, const string & rhs);
2.4.4 string的遍历(重点)
string实际上也可以看作是一种存储char型数据的容器。
-
通过下标遍历:
//使用下标遍历 for(size_t idx = 0; idx < str.size(); ++idx){ cout << str[idx] << " "; } cout << endl;
注意:使用
[]
的时候并不检查索引是否是有效的,如果超出范围,会引起未定义行为。而函数at()
会检查,如果使用at()
的时候索引无效,会抛出out_of_range
异常。string str("hello"); cout << str.at(4) << endl; //输出o cout << str.at(5) << endl; //运行时抛出异常
-
增强for循环遍历: 针对容器,可以使用增强for循环进行遍历其中的元素。增强for循环经常和auto关键字一起使用,auto关键字可以自动推导类型。
for(auto & ch : str){ cout << ch " "; } cout << endl;
-
迭代器方式进行遍历:
begin函数返回首迭代器。
end函数返回尾迭代器。
//某容器的迭代器形式为 容器名::iterator
//此处auto推导出来it的类型为string::iterator
auto it = str.begin();
while(it != str.end()){
cout << *it << " ";
++it;
}
cout << endl;
2.5 C++动态数组
C++中,std::vector(向量)是一个动态数组容器,能存放任意类型的数据。
🔔其动态性体现在以下几个方面:
- 动态大小:
std::vector
可以根据需要自动调整自身的大小。 - 动态插入和删除:
std::vector
允许在任意位置插入或删除元素,而不会影响其他元素的位置。 - 动态访问:
std::vector
提供了随机访问元素的能力。可以通过索引直接访问容器中的元素,而不需要遍历整个容器。这使得对元素的访问具有常数时间复杂度O(1)
,无论容器的大小如何。
2.5.1 vector的构造
vector常用的几种构造形式:
- 无参构造,仅指明vector存放元素的种类,没有存放元素;
- 传入一个参数,指明vector存放元素的种类和数量,参数是存放元素的数量,每个元素的值为该类型对应的默认值;
- 传入两个参数,第一个参数为vetor存放元素的数量,第二个参数为每个元素的值(相同);
- 通过列表初始化vector,直接指明存放的所有元素的值
- 迭代器方式初始化vector,传入两个迭代器作为参数,第一个为首迭代器,第二个为尾后迭代器;
代码:
vector <int> numbers;
vector<long> numbers2(10); //存放10个0
vector<long> numbers2(10,20); //存放10个20
vector<int> number3{1,2,3,4,5,6,7};
vector<int> number3{1,2,3,4,5,6,7};
vector<int> number4(number3.begin(),number3.end() -3);
2.5.2 vecoor的常用操作
iterator begin(); //返回首位迭代器
iterator end(); //返回尾后迭代器
bool empty() const; //判空
size_type size() const; //返回容器中存放的元素个数
size_type capacity() const; //返回容器容量(最多可以存放元素的个数)
void push_back(const T& value); //在最后一个元素的后面再存放元素
void pop_back(); //删除最后一个元素
void clear(); //清空所有元素,但不释放空间
void shrink_to_fit(); //释放多余的空间(可以存放元素但没有存放运算的空间)
void reserve(size_type new_cap);//申请空间,不存放元素
2.5.3 vector的动态扩容
当vector存放满后,仍然追加存放元素,vector会进行自动扩容。
🔔 vector扩容的操作步骤:
- 开辟空间
- Allocator分配
- 复制,再添加新元素
- 回收原空间
2.5.4 vector的底层实现(重点)
利用sizeof查看vector对象的大小时,发现无论存放的元素类型、数量如何,其大小始终为24个字节(64位环境)因为vector对象是由三个指针组成。
🔔_start指向当前数组中第一个元素存放的位置
🔔_finish指向当前数组中最后一个元素存放的下一个位置
🔔_end_of_storage指向当前数组能够存放元素的最后一个空间的下一个位置
可以推导出:
size():_finish - start
capacity():_end_of_storage - star
第三章 C++输入输出流
3.1 C++ 输入输出流的概念
3.1.1 输入输出的含义
程序的输入指的是从输入文件将数据传送给程序(内存)。
程序的输出指的是从程序(内存)将数据传送给输出文件。
万物皆文件
3.2.2 C++常用流的类型
C++ 的输入与输出包括以下3方面的内容:
- 标准 I/O: 对系统指定的标准设备的输入和输出。即从键盘输入数据,输出到显示器屏幕。
- 文件 I/O: 以外存磁盘文件为对象进行输入和输出,即从磁盘文件输入数据,数据输出到磁盘文件。
- 串 I/O: 对内存中指定的空间进行输入和输出。通常指定一个字符数组作为存储空间(实际上可以利用该空间存储任何信息)。缓冲区。
🔔常用的输入输出流如下:
类名 | 作用 | 头文件 |
---|---|---|
istream | 通用输入流 | iostream |
ostream | 通用输出流 | iostream |
iostream | 通用输入输出流 | iostream |
ifstream | 文件输入流 | fstream |
oftream | 文件输出流 | fstream |
fstream | 文件输入输出流 | fstream |
istringstream | 字符串输入流 | sstream |
ostringstream | 字符串输出流 | sstream |
stringstream | 字符串输入输出流 | sstream |
3.2 流的四种概念(重点)
输入输出流可能会发生错误,这些错误有些是可以恢复的,有些是不可以恢复的,因此要有一个能表示I/O状态的参数。
在C++标准库中,用iostate
来表示流的状态。
🔔不同编译器可能有不同的状态,但是一般只有四种:
badbit
表示发生系统级别的错误: 如不可恢复的读写错误,一旦badbit
被置位,流就无法再使用了。failbit
表示可回复的错误: 如期望读取一个数值,却读出一个字符等错误。这种问题通常是可以修改的,流还可以继续使用。eofbit
表达到达流结尾的位置: 此时eofbit
和failbit
都会被置位。goodbit
表示流处于有效状态: 流在有效状态下,才能正常使用。如果badbit
、failbit
和eofbit
任何一个被置位,则流无法正常使用。
这四种状态都定义在类 ios_base 中,作为其数据成员存在。在 GNU GCC7.4 的源码中。
3.3 标准输入输出流
C++标准库定义了三个预定义的标准输入输出流对象,分别是 std::cin
、std::cout
和std::cerr
。它们分别对应于标准输入设备(通常是键盘)、标准输出设备(通常是显示器)和标准错误设备(通常是显示器)。标准输入、输出的内容包含在头文件iostream中。
3.3.1 标准输入流
void printStreamStatus(std::istream & is){
cout << "is's goodbit:" << is.good() << endl;
cout << "is's badbit:" << is.bad() << endl;
cout << "is's failbit:" << is.fail() << endl;
cout << "is's eofbit:" << is.eof() << endl;
}
void test0(){
printStreamStatus(cin); //goodbit状态
int num = 0;
cin >> num;
cout << "num:" << num << endl;
printStreamStatus(cin); //进行一次输入后再检查cin的状态
}
如果没有进行正确的输入,输入流会进入failbit
的状态,无法正常工作,需要回复流的状态。
🔔恢复流的状态的原理: 利用clear
和ignore
函数配合。
🔔恢复流状态的代码:
if(!cin.good()){
//恢复流的状态
cin.clear();
//清空缓冲区,才能继续使用
cin.ignore(std::numeric_limits<std::streamsize>::max(),'\n');
cout << endl;
printStreamStatus(cin);
}
3.3.2 标准输出流
导致输出缓冲区刷新的三种情况:
- 程序正常结束。
- 缓冲区满。
- 使用操纵符显式地刷新输出缓冲区,如
endl
。
🔔GCC中标准输出流的默认缓冲区大小就是1024个字节。
标准输出的操作符:
enl
:用来完成换行,并刷新缓冲区ends
:在输入后加上一个空字符\0
,然后再刷新缓冲区flush
:直接刷新缓冲区
3.3.3 标准错误输出流
标准错误流不带缓冲
#include <unistd.h>
void test1(){
cerr << 1;
cout << 3;
sleep(2);
}
3.3.4 缓冲机制
为什么要引入缓冲区?
缓冲区就是一块内存区,它用在输入输出设备和 CPU 之间,用来缓存数据。它使得低速的输入输出设备和高速的CPU 能够协调工作,避免低速的输入输出设备占用CPU,减少磁盘的读写次数,提高计算机的运行速度,
缓冲机制分为三种:
- 全缓冲: 当填满缓冲区后才进行实际 I/O 操作。全缓冲的典型代表是对磁盘文件的读写。
- 行缓冲: 当在输入和输出中遇到换行符时,执行实际的 I/O 操作。典型代表是
cin
。 - 不带缓冲: 不进行缓冲,有多少数据就刷新多少。标准错误输出 cerr是典型代
表,这使得出错信息可以直接尽快地显示出来。
3.4 文件输入输出流(重点)
C++ 对文件进行操作的流类型有三个:
ifstream
文件输入流ofstream
文件输出流fstream
文件输入输出流
构造函数形式:
ifstream();
explicit ifstream(const char* filename, openmode mode = ios_base::in);
explicit ifstream(const string & filename, openmode mode =
ios_base::in);
ofstream();
explicit ofstream(const char* filename, openmode mode = ios_base::out);
explicit ofstream(const string & filename, openmode mode =
ios_base::out);
fstream();
explicit fstream(const char* filename, openmode mode = ios_base::in|out);
explicit fstream(const string & filename, openmode mode =
ios_base::in|out);
🔔explicit关键字的意义 —— 禁止隐式转换
3.4.1 文件输入流
文件输入流对象的创建:
#include <fstream>
void test0(){
ifstream ifs;
ifs.open("test1.cc");
ifstream ifs2("test2.cc");
string filename = "test3.cc";
ifstream ifs3(filename);
}
🔔文件模式:
in
:只读,文件不存在,打开失败。out
:只写,文件不存在,创建一个。app
:追加,写入将始终发生在文件的末尾。ate
:定位到文件末尾,但是不能直接写入。ios::ate|ios::in
和ios::app
是一样的效果。
按行读取:
string line;
while(getline(ifs,line)){
cout << line << endl;
}
读取指定字节数的内容:
通过文件输入流对象读取到的内容交给字符数组,同时需要传入要读取的字符数。
basic_istream & read(char_type* s, std::streamsize count);
//参数s:指向要存储字符到的字符数组的指针
//参数count:要读取的字符数
tellg();
//获取文件游标的位置
seekg(size_t pos);//设置绝对位置
seekg(off, dir);//设置相对位置,off代表偏移量,dir代表锚点(文件游标偏移的基准点)
//设置游标位置
锚点(dir):
常量 | 解释 |
---|---|
beg | 流的开始 |
end | 流的结尾 |
cur | 流位置的当前位置 |
读取一个文件的全部内容:
void test0(){
string filename = "test.cc";
ifstream ifs(filename);
if(!ifs){
cerr << "ifs open file fail!";
return;
}
//读取一个文件的所有内容先要获取文件的大小
//将游标放到了文件的最后(尾后)
fs.seekg(0,std::ios::end);
long length = ifs.tellg();//获取尾后下标,实际就是总的字符数
cout << length << endl;
char * pdata = new char[length]();
//需要将游标再放置到文件开头
ifs.seekg(0,std::ios::beg);
ifs.read(pdata,length);
//content包含了文件的所有内容,包括空格、换行
string content(pdata);
cout << "content:" << content << endl;
/* cout << pdata << endl; */
ifs.close();
3.4.2 文件输出流
文件输出流的两种方式:
- 通过输出流运算符写内容
//每次写入都会覆盖
string filename = "test3.cc";
ofstream ofs3(filename);
string line("hello,world!\n");
ofs << line;
ofs.close();
//通过追加的方式写入文件
string filename = "test3.cc";
ofstream ofs3(filename,std::ios::app);
- 通过write函数写内容(和linux中的文件符操作类似)
char buff[100] = "hello,world!";
ofs.write(buff,strlen(buff));
3.5 字符串输入输出流
字符串I/O是内存中的字符串对象与字符串输入输出流对象之间做内容传输的数据流,通
常用来做格式转换。
C++ 对字符串进行操作的流类型有三个:
1.istringstream
字符串输入流
2. ostringstream
字符串输出流
3. stringstream
字符串输入输出流
构造函数:
istringstream(): istringstream(ios_base::in) { }
explicit istringstream(openmode mode = ios_base::in);
explicit istringstream(const string& str, openmode mode = ios_base::in);
ostringstream(): ostringstream(ios_base::out) { }
explicit ostringstream(openmode mode = ios_base::out);
explicit ostringstream(const string& str, openmode mode =
ios_base::out);
stringstream(): stringstream(in|out) { }
explicit stringstream(openmode mode = ios_base::in|ios_base::out);
explicit stringstream(const string& str, openmode mode =
ios_base::in|ios_base::out);
3.5.1 字符串输入流
将字符串s的内容传给了两个int型数据,输入流运算符会默认以空格符作为分隔符。
void test0(){
string s("123 456");
int num = 0;
int num2 = 0;
//将字符串内容传递给了字符串输入流对象
istringstream iss(s);
//istringstream中的第二个参数是用来设置分隔符的种类的
//istringstream iss(s,"\n"); //以换行符为分隔符
iss >> num >> num2;
cout << "num:" << num << endl;
cout << "num2:" << num2 << endl;
}
3.5.2 字符串输出流
将各种类型的数据转换成字符串类型,存在字符串输出流的缓冲区中,利用str函数,全部转为string类型并完成拼接。
void test0(){
int num = 123, num2 = 456;
ostringstream oss;
//把所有的内容都传给了字符串输出流对象
oss << "num = " << num << " , num2 = " << num2 << endl;
cout << oss.str() << endl;
}