C++学习笔记
引用📖
1. 引用的定义
int a = 3;
int &r = a; // 此时叫做r引用a, r是a的别名
int b = 4;
r = b; //此时只相当于将 b 的值赋给 a;
2. 引用的作用
- 当 r 引用了 a , 对 r 修改对 a 也会生效
r = 7;
cout << a << endl;
cout << t << endl;
/* 输出:
7
7
*/
- 引用也可以引用 " 引用 "
int &r_to_r = r;
// 对 r_to_r 修改也是一样的效果, 和 r 同时引用了 a
3. 常引用
- 不能通过常引用去修改其引用的值
int a = 3;
const int &r = a;
r = 7; //这样是不行的, 编译出错
a = 7; //可以修改本身
-
常引用和非常引用的关系
- const ElemType & 和 ElemType & 是不同的类型
- ElemType & 或 ElemType 的变量可以用来初始化 const ElemType &
int a = 3; int &r = a; const int & r_static = r;
- 但const ElemType & 不能用来初始化 ElemType &
int a = 3; const int &r_static = a; int &r = r_static; // 编译出错 // 因为r_static为常引用(不可修改引用值), 不能通过另一个引用来试图修改
4. 实际运用
- 使用指针修改
void swap(int* a, int* b){
int temp = *a; *a = *b; *b = temp;
}
swap(&a, &b);
- 使用引用修改
void swap(int &a, int &b){
int temp = a; a = b; b = temp;
}
swap(a, b);
5. 奇怪的返回值
int& getElement(int * a, int i)
{
return a[i]; // 定义返回值是引用类型
}
int main()
{
int a[] = {1,2,3};
getElement(a,1) = 10; // 将返回的a[1]的引用修改为10
cout << a[1] ; // 输出 10
return 0;
}
动态内存分配🔥
1. new的使用
- 申请一个类型为ElemType的变量内存
int *p;
p = new int; // 此时的p指向大小为sizeof(int)的内存空间
*p = 4; //尝试赋值
cout << p << endl; //输出结果是: 4
- 使用new申请一片连续内存空间(数组)
int *p_to_arr;
int n = 5;
p_to_arr = new int[n*20] //可以是整型表达式
*p_to_arr = 4; // p_to_arr 为指向数组首地址的指针
cout << p_to_arr[0] << endl; // 输出: 4
2. delete的使用
- 释放单个变量空间
delete p;
- 释放一片连续内存空间
delete[] p_to_arr;
delete p_to_arr; // 这样是错的, 只释放了单个空间
内联函数, 重载函数⚔️
1. 内联函数
- 定义
inline int Max(int a, int b){
return a > b ? a : b; //返回最大值
}
- 实际表现
k = Max(3, 4);
// 等价于如下代码段
tmp = 3 > 4 ? 3 : 4; // 以变量来存储return的值
k = tmp;
2. 函数重载
例如:
(1) int Max(double f1, double f2){}
(2) int Max(int n1, int n2){}
(3) int Max(int n1, int n2, int n3){}
Max(3.2, 1.2); //调用(1)
Max(1, 2); //调用(2)
Max(1, 4, 2); //调用(3)
Max(1, 2.2); //无法分辨, 编译出错
// 如下也分辨不了, 因为都可以没有参数
int max(int x = 0){}
void max(){}
优点:
- 使得函数的命名变得简单
- 易于函数调用, 编译器根据参数的类型和个数来判断为哪一个Max
3. 函数的缺省参数
void func(int x1, int x2 = 2, int x3 = 3){}
func(1); //符合
func(2); //符合
func(2,,3) //编译出错,编译器无法分辨省了哪个参数
优点
- 提高了程序的扩充性, 如果你想要为某个功能添加特色时, 只需要多给一个参数, 而其他地方皆可以保持不变
类和对象
1. C++中类的定义
class CRectangle; // 可以先声明一个类, 然后再定义
class CRectangle{
public: //
int h, w;
void set_val(int a, int b){
w = a; w = b;
}
int calArea(){
return w*h;
}
int calPeri(){
return 2*(w+h);
}
}; // 记得加分号
2. 使用方法
- 实例化对象
CRectangle r; // 像类型一样使用, 和结构体很相似
- 访问成员
r.set_val(3, 4); // 设置宽和高分别为3, 4
cout << r.w << endl; // 输出 3
3. 指针, 引用
// 指针
CRectangle r;
CRectangle* pr = &r;
p->set_val(3, 4);
p->w;
// 引用
CRectangle &rr = r;
rr.set_val(3, 4);
rr.w;
4. 修饰符
- private: 私有属性, 只能通过内部方法来访问
- public: 公有属性, 可以直接访问
- protected: 保护属性, 目前未知
// 公有的szName
strcpy(Person.szName, "Tom123456789")
// 私有的
Person person;
person.setName("Tom123456789")
// 假设我们修改内部属性szName长度为5, Tom123...显然越界
/*
1. 对于公有的, 我们要一一改动语句
2. 而私有的, 我们只需要在内部函数加入一个判断即可
*/
-
成员函数的功能
- 可以访问当前对象的全部属性, 函数
- 也可以访问同类的其他对象的全部属性, 函数
int add(A obj1, A obj2){ return obj1.x + obj2.x // 可以是私有属性 }
5. 构造函数
构造函数在实例化对象时会被自动调用, 一般用于初始化对象的一些属性变量, 和Python的__init__方法类似, 有利于程序的运行
注意: 构造函数并不会分配内存空间, 分配在对象生成时就已经做了, 构造函数只是初始化!
- 构造函数名字要和类名相同, 可以有参数
- 构造函数不需要返回值, 不需要写返回值类型(void都不行)
class Test{
Test(int a, int b); // 此处声明构造函数Test, 可在外部定义
}
-
一个函数可以有多个构造函数(不是重载) ==> 类型转换构造函数
-
该函数和构造函数极其相似, 可以认为是特殊的构造函数
-
如:
-
Test a = 2; 这里只是调用了构造函数
-
class Test{ public: int val; Test(){} //构造函数默认为空 Test(int a){ val = a; } } int main(){ Test a; a = 2; //这里就调用了类型转换构造函数 a.getVal(); return 0; }
-
🔥 类型转换构造函数本质是: 等号右边其他类型的值, 转化为一个临时对象, 并把该值赋给等号左边的对象, 之后会消亡
-
-
-
如果没有写构造函数, 则编译器默认生成一个无参数的构造函数
-
构造函数支持重载
Test::Test(){}
Test::Test(int a){}
Test::Test(int a, int b){}
构造函数被调用的几种情况(假设构造函数有参数)
-
实例对象
-
Test a(1); 正确
-
Test a; 错误, 没有加参数
-
Test a = 2; 正确
-
Test a; 🔥
a = 2; 错误, a = 2并非初始化语句
-
-
定义类的指针
- Test* p = new Test(1); 正确
- Test* p = new Test; 错误
- Test* p = new Test[2]; 错误, 但未知解决方法
- 对象数组
- Test array[2] = {1, 2}; 正确
- Test array[2] = {Test(1), Test(2)} 正确
- Test array[2]; 错误, 没有参数
- 类指针数组
- Test *p[2] = {new Test(1), NULL} 正确, 因为是指针, 不是对象
- Test *p[2] = {new Test, NULL} 错误, 无参
6. 复制构造函数
定义:
- 复制构造函数和构造函数类似, 但前者只有一个参数, 且为该类的引用对象
- 形如Test::Test( Test& x) 或 Test::Test( const Test& x) 二选一, 只能定义一个复制构造函数, 后者可以引用常量
- 如果没有复制构造函数, 编译器会默认生成
class Test{
public:
int val;
Test(int a){ // 构造函数
val = a;
}
Test(Test& a){ // 复构函数
val = a.val;
cout << "Called" << endl;
}
};
因为, 这涉及到函数的传参机制与复制构造函数的关系. 如果采取该种方式, 当调用复制构造函数是, 首先要进行传参, 而传参又要调用复制构造函数, 如此反复 ==> 死递归
复制构造函数被调用的几种情况
- 当一个对象以另一个同类的对象值初始化时
Test a1(2);
Test a2(a1); // 此时会调用复构函数
// 等价于 Test a2 = a1; 是初始化, 不是赋值语句哦!
易错:
Test a = 2;
Test b = 1;
b = a; // 一个对象被另一个对象赋值时不会调用复制构造函数
- 当对象作为某函数的参数传参时
void func(Test a){}
Test a(2);
func(a); // 此时会调用复构函数
- 当对象作为函数的返回值时, 会生成一个临时对象func()
Test func(int a){
Test x(a);
return x;
}
cout << func(3).val << endl;
/*输出: Called
3
*/
注意: 在dev C++中, 函数返回值并不会调用复制构造函数, 而是直接返回
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D2LzIcec-1643544309015)(C:\Users\a\AppData\Roaming\Typora\typora-user-images\image-20220116194614953.png)]
7. 析构函数
定义:
- 当一个对象消亡时析构函数就会被调用
- 函数名和类名相同, 没有参数和返回值, 在名字前加一个 ~ 表示析构函数
class Test{
public:
~Test(){
printf("destructor called!\n");
}
}
注意: 析构函数并不是销毁内存, 只是在销毁前做一些事情
对象消亡的几种情况
- 程序结束, 所有的变量销毁
- 作为局部变量的结束
- 调用函数生成的对象的结束
具体实例
Test func(Test lobj){
return lobj; // 1. 局部变量lobj的消亡
}
int main(){
Test obj;
obj = func(obj); // 2. 函数调用结束生成对象的消亡
return 0; // 3. 程序结束时obj的消亡
}
/*输出结果:
destructor
destructor
destructor
*/
特例: 当动态申请(new)了某个内存却不销毁时, 程序结束它也不会消亡
8. this指针
在C++中的类与对象的概念其实可以翻译为C语言的结构体, 可以认为C++代码是先翻译为C语言再编译的
class Test{ // C++
public:
int val;
void setVal(int e){
val = e;
}
};
struct Test{ // C语言
int val;
};
// 重点在于成员函数的翻译, 通过使用指向该类型的指针来"绑定"成员函数
void setVal(struct Test* this, int e){
this->val = e;
}
实际运用: 非静态成员函数中可以直接使用this来代表其作用的对象
class Complex{
public:
int real, imag;
void Print(){
cout << real << "," << imag;
}
// 列表?
Complex(double r, double i):real(r), imag(i){}
Complex AddOne(){
this->real++; // 等价real++, 要this干嘛???
this->Print(); // 等价Print()
return *this; // 解引用返回其作用的对象
}
};
int main(){
Complex c1(1, 1), c2(0, 0);
c2 = c1.AddOne();
return 0;
}
易错误区:
class Test{
int i;
public:
void Hello(){ cout << "hello" << endl;}
};
Test* p = NULL; // 类型为 Test 的空指针
试判断 p->Print() 正误?
- 正确, 可以理解为类对象吧
翻译为C语言: void Hello(Test* p){cout << “hello” << enld;}
传入了该指针, 虽然是空的, 但是没有使用到对象任何的成员, 所以正确 还没有真正理解…
- 如果改为 void Hello(){ cout << i << endl;} 就错了, p并没有指向任何对象, 只是一个该类类型的空指针
9. 静态成员
与普通成员区别:
- 每个对象的普通成员变量各不相同, 却 共享 静态成员变量
- 普通成员函数必须作用于某个对象, 而静态成员函数与其并不相关
- 静态成员不需要通过对象可以直接访问 => Test::printH(); 👇
优点: 目的是将一些与某些类相关的全局变量 / 函数写到类的定义里, 利于代码的维护和理解
注意:
- 在定义了静态成员变量后, 应在全局作用域再定义初始化一次, 使该变量与类相连接
class Test
{
public:
static int real, imag;
static void printH()
{
cout << "Hello World" << endl;
}
Test() {}
};
int Test::real = 0;
int Test::imag = 0;
- 之后就可以直接使用, Test::real 来访问静态成员变量了, 私有的不能直接访问哦
- 静态成员函数不能访问非静态成员变量, 因为不绑定任何对象, 无法判断是谁的成员
- 也不能调用非静态成员函数, 万一这函数里访问了非静态成员变量呢 😃
运用实例:
class CRectangle{
private:
int w, h;
static int nTotalArea; // 记录总数
static int nTotalNumber; // 记录总面积
public:
CRectangle(int w_, int h_);
~CRectangle();
static void PrintTotal(); // 用于打印数量, 面积总和
};
CRectangle::CRectangle(int w_, int h_){
w = w_; h = h_;
nTotalNumber++;
nTotalArea += w * h;
}
CRectangle::~CRectangle(){ // 这里有缺陷
nTotalNumber--;
nTotalArea -= w * h;
}
void CRectangle::PrintTotal(){
cout << nTotalNumber << ',' << nTotalArea << endl;
}
// 初始化静态成员变量, 私有也能直接初始化吗...
int CRectangle::nTotalNumber = 0;
int CRectangle::nTotalArea = 0;
int main(){
CRectangle r1(3, 3), r2(2, 2);
// cout << CRectangle::nTotalNumber << endl; <= 错的, 不能直接访问私有成员
CRectangle::PrintTotal();
r1.PrintTotal();
return 0;
}
10. 成员对象和封闭类
通俗来讲: 就是定义类的时候, 有一个成员是其他类的对象, 那么该类就叫做封闭类
实例讲解:
class Tyre{
private:
int radius;
int width;
public:
Tyre(int r, int w):radius(r), width(w) {} // 初始化列表
};
class Engine{ };
class Car{ // 成员会包括轮胎, 引擎, 为封闭类
private:
int price;
Tyre tyre; // 这叫做声明, 并没有初始化, 这里也不用初始化
Engine engine;
public: // 使用封闭类的构造函数, 对成员对象初始化
Car(int p, int tr, int w):price(p), tyre(tr, w) {}
};
int main(){
Car car(20000, 17, 225);
return 0;
}
注意: 如果Car没有定义构造函数, 编译器调用默认的会报错, 因为成员对象需要初始化
初始化列表: 列表中的参数可以是任何有意义的参数, 可以是任意复杂表达式, 或函数
封闭类的构造函数和析构函数的执行顺序:
- 构造封闭类对象前, 先一层一层的从内部构造成员类对象
- 内部的成员对象构造顺序, 按照声明的顺序
- 对于析构函数, 按照栈的顺序, 先构造的后析构
封闭类的复制构造函数:
封闭类的默认复构函数, 就是调用封闭类中的成员对象所属类的复构函数
因为, 如果封闭类的对象是用复制生成的, 里面的成员对象就还没初始化, 你必须调用成员对象所属类的复构函数, 或者自定义使用初始化列表初始化
举例:
// 假设存在一个A类有B类的成员对象则A类的复构函数应如此写
A(const A& obj):obj_B(obj.obj_B) {}
// 而不是像下面这样写
A(const A& obj){
obj_B = obj.obj_B; // 他会认为你没有初始化而是直接赋值
}
11. 常量对象, 常量成员函数
示例:
- 定义常量对象 -> const Test obj;
- 定义常量成员函数 -> void func() const {}
- 常量成员函数不能修改成员变量, 也不能调用成员函数, 万一函数里修改了呢
- 但是可以修改静态变量的值, 和调用静态成员函数
两者关联: 常量对象可以调用常量成员函数, 不能调用非常量成员函数
注意: 如果两个成员函数 名字 和 参数表都一样, 但是一个有const, 这两个函数是重载关系
12. 友元
友元分为 友元函数 和 友元类 两种
友元函数: 可以是类的成员函数(包括构造函数, 析构函数), 也可以是全局函数
- 一个类的友元函数可以访问该类的私有成员
class CCar {
private:
int price;
// public: 友元不是公有的, 是单独的
friend int MostExpensiveCar(CCar cars[], int total);
friend void CDriver::ModifyCar(CCar* pCar)
}
void CDriver::ModifyCar(CCar* pCar){
cout << pCar->price << endl; // 可以直接访问私有成员
}
- 如果B类的友元类是A类, 那么A类的成员函数可以访问该类的私有成员
class CCar{
private:
int price;
friend class CDriver; // 声明为友元类
}
运算符重载
运算符重载的形式:
- 运算符重载的实质是函数的重载
- 可以重载为普通函数, 也可以重载为成员函数
- 编译过程中将含运算符的表达式, 转化为对运算符函数的调用
1. 算术运算符的重载
重载声明形式:
返回值类型 operator 运算符符号 (参数表) { }
- 重载为普通函数, 参数为两目
Complex operator+( const Complex& a, const Complex& b){
return Complex(a.val+b.val); // 返回由两个对象相加构成的新对象
}
- 重载为成员函数, 参数为一目
Complex Complex::operator-( const Complex& obj){
return Complex(val-obj.val);
}
a + b; // 等价于operator+(a, b);
a - b; // 等价于a调用了成员函数operator-(b);
如下例:
// 如果只定义了成员函数
Complex Complex::operator + ( double r ){
return Complex(r + val);
}
// 表达式c = c + 5 是可以的, 5 作为成员函数的参数
// 表达式c = 5 + c 是错误的, 这里就需要使用普通函数,来传两个参数
2.赋值运算符的重载
重载声明形式和普通算符运算符一致, 但是只能重载为成员函数
下面以定义String类为例:
class String {
private:
char* str;
public:
String():str(new char[1]) { str[0] = 0; } // 简单的初始化一下
const char* c_str() { return str; } // 便于打印成员值
// 当被常量字符串赋值时
String& operator = (const char* s);
~String() { delete[] str; }
};
- 当被常量字符串赋值时
String& String::operator= (const char* s){
delete[] str; // 释放原先的值
str = new char[strlen(s)+1]; // new一个新的空间来存放
strcpy( str, s );
return *this; // 返回自身
}
- 被同类对象赋值
坏处:
- 当A / B被修改时, 另一个也会被修改
- A / B 被常量字符串重新赋值时, 会销毁另一个的空间
- 当A消亡后, 空间已经被释放了, B无法使用, 而且B消亡时会再次释放那片空间导致报错
终上所述: 我们需要自定义同类对象间的赋值运算符
String& operator= (const String& s){
delete[] str;
str = new char[strlen(s.str)+1]; // 成员函数可以访问私有成员
strcpy(str, s.str);
return *this; // 这里又返回自身了
}
返回自身, 并且类型是String& 的原因是:
- 当a = b = c; 时, 要求将b = c的返回值赋给a, 因此需要返回自身
- 当(a = b) = c; 时, 要求将c 赋给 a = b 的返回值, 因此返回值类型是String&
对运算符重载时, 好的习惯是保留运算符原有的特性
- 自身赋值给自身, s = s; 会出现先释放了空间, 又访问的情况, 改动如下
String& operator= (const String& s){
if( p == s.p ){ // 如果是自己, 就直接返回
return *this;
}
delete[] str;
str = new char[strlen(s.str)+1]; // 成员函数可以访问私有成员
strcpy(str, s.str);
return *this; // 这里又返回自身了
}
- 在调用String类复制构造函数时, 也会出现共用同一片空间的情况, 改动如下:
String(const String& s){
// delete[] str; 构造初始化的时候, 就不用删除了?
str = new char[strlen(s) + 1];
strcpy(str, strlen)
}
3. 重载为友元函数
定义成员函数不能使用 数字 + 对象 的表达式, 普通函数又不能访问对象的私有成员, 因此需要重载为友元函数
friend Complex operator+(double r, const Complex& c);
4. 实现可变长数组类的几个知识点
- 缺省参数, 不能当做初始化列表参数, 但是可以声明然后起作用
CArray(int s = 0); // s代表数组元素个数
CArray::CArray(int s):size(s) { //声明就有用的啊???
if(s == 0){
ptr = NULL;
}
else{
ptr = new int[s];
}
}
- 中括号运算符 [ ] 的重载
int& CArray::operator[] (int i){
return ptr[i];
}
- push_back的实现
void CArray::push_back(int val) {
if(size >= maxSize){
maxSize += 32;
int* tmpPtr = new int[maxSize];
memcpy(tmpPtr, ptr, sizeof(int) * size);
delete[] ptr;
ptr = tmpPtr;
}
else if(!ptr) {
maxSize = 32;
ptr = new int[maxSize];
}
ptr[size++] = val; // 将值放在尾部同时长度加一
}
5. 流运算符的重载
cout 是在ostream类的对象, cin即istream类的对象, 可以直接调用的
下面以**cout 输出 5 / “hello”**为例:
- 重载为ostream的成员函数
void ostream::operator<< (int n) {}
前面我们提到了, 对运算符的重载要尽量保留其原有特性
如: cout << 5 << “hello”; 我们应如何实现?
ostream& ostream::operator<< (int n) {}
cout << 5 << “hello” 的本质就是 => cout.operator<<(5).operator<<(“hello”);
- 重载为普通函数
如下例:
class CStudent {
private:
int age;
public:
CStudent(){age = 5;}
// 从如下声明可见, 有时还需声明为友元函数来访问其私有成员
friend ostream& operator<<(ostream& o, CStudent& student);
};
// 重载<<符号
ostream& operator<<(ostream& o, CStudent& student){
// 这里之所以是引用ostream类对象, 我认为是避免 递归 的出现
o << student.age; // 该函数为友元, 所以可以访问其私有成员
return o;
}
int main(){
CStudent student;
cout << student;
return 0;
// 输出结果: 5
}
6. 类型转换运算符的重载
定义形式:
operator double() {}
在定义类型转换运算符的时候, 不需要写返回值类型, 因为需要返回重载的类型, 也不需要参数, 因为参数只有它本身.
调用的情况:
- (double)variable 使用强制类型转换运算符时
- double x = 2 + obj; 在和基本数据类型运算时, 会自动调用函数先将对象转化为相应的类型, 再相加
- int x = 2 + obj; 虽然此时没有重载Int类型的转换符, 但是编译器会将重载后的double类型, 转化为int类型
等价于 x = 2 + Class.operator double()
7. 自增, 自减运算符的重载
- 重载前置++, 返回对象的引用
// 作为成员函数
Test& operator++();
// 作为全局函数
Test& operator++(Test& obj);
- 重载后置++, 返回临时对象
// 作为成员函数
Test operator++(int k); // 多出一个无用的参数
// 作为全局函数
Test operator++(Test& obj, int k);
完整函数定义:
- 前置++
Test& Test::operator++(){
++n;
return *this;
}
- 后置++
Test Test::operator++(int k){
Test temp(*this);
n++;
return temp; // 返回临时对象
}
8. 运算符重载的注意事项
- C++不允许定义新的运算符
- 重载后的运算符含义应符合日常习惯,如+表示加法的意思, 不是减法
- 运算符重载不改变运算符的优先级
- 以下运算符不能被重载, (" . ", " .* ", " :: ", " ?: ", " sizeof ")
- 重载运算符(), [], ->或者赋值运算符 = 时, 重载函数必须声明为类的成员函数
继承和组合
1. 类的继承
继承的使用形式: class 派生类名: public 基类名 {};
2. 派生类
基本特点:
- 派生类是通过对基类进行修改和扩充得到的
- 定义了派生类后, 可以独立使用
- 派生类拥有基类的全部成员(变量和函数), 不论是private, protected, public
- 派生类无法访问基类的private成员
内存空间
- 内存空间等于 基类对象的成员 加上 派生类对象添加的成员 的体积之和
- 基类对象的存储位置位于派生类对象新增的成员变量之前
可实现功能:
class base {
public:
string name;
void setInfo(const string& name_);
void printInfo();
};
class Derived: public base {
public:
int nAge;
void setInfo(const string& name_, int nAge_){
base::setInfo(name_);
nAge = nAge_;
}
void printInfo(){
base::printInfo(); // 使用基类的
cout << nAge << endl;
}
};
3. 类之间的两种关系
- 继承 => 如果一个B类是A类的派生类, 逻辑上应满足 " 一个B对象也是一个A对象 "
- 组合(复合) => 如果一个类的有一个成员是其它类的对象(封闭类), 就叫做是组合关系
如何使用继承和组合
- 继承
示例:
如果一个CMan代表男人, 现需定义一个CWoman类, 而CMan和CWoman又有很多共同特点, 那么CWoman是否应该有CMan派生而来 错误
所以应该抽象出两个类的共同特点, 以此为基类, 派生出CMan和CWoman类
- 组合
class CPoint {
double x, y;
friend class CCircle;
};
/* 继承 (错误做法) */
class CCircle: public CPoint {
double r;
};
/* 组合 */
class CCircle {
double r;
CPoint center; // 作为圆心独特存在
};
- “小狗” 和 “业主” 关系类定义
class CDog;
class CMaster {
CDog* dogs[10];
};
class CDog {
CMaster* m;
}
4. 覆盖
访问成员:
- 直接使用名字访问的是派生类的成员
- 可以通过使用基类明加上作用域符号::, 来访问基类的成员
一般来说, 派生类中不定义同名成员变量, 覆盖成员函数情况较多
5. 修饰符
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A6Yku6aA-1643544309018)(C:\Users\a\AppData\Roaming\Typora\typora-user-images\image-20220129172510397.png)]
6. 派生类的构造函数
一般通过初始化列表, 调用基类构造函数进行基类对象成员的初始化
代码示例:
class Bug {
int legs;
int color;
public:
Bug(int legs_, int color_){
legs = legs_;
color = color_;
}
};
class FlyBug: public Bug { // 派生类
int wings;
public:
// 使用如下初始化列表构造派生类对象
FlyBug(int legs_, int color_, int wings_):Bug::Bug(legs_, color_){
wings = wings_;
}
};
构造, 析构函数调用顺序
该顺序和封闭类相似
- 先调用基类的构造函数
- 如果有成员对象, 再调用成员对象的构造函数
- 最后调用派生类的构造函数
7. public继承的赋值规则
- 派生类对象可以赋值给基类对象
b = d;
2. 派生类对象可以初始化基类对象的引用
base& br = d;
3. 派生类对象的地址可以赋值给基类对象的指针
base* pb = &d;
- 只能通过该指针访问派生类中继承基类的成员
- 可以通过强制类型转化, 将 基类指针变量 类型转化为 派生类指针变量 类型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HSpPzPB7-1643544309019)(C:\Users\a\AppData\Roaming\Typora\typora-user-images\image-20220129194742876.png)]
8. 多重继承
- 类A是类B的直接基类
- 类A是类C的间接基类
- 类B是类C的直接基类
在定义派生类时, 只需要列出其直接基类, 派生类会沿着类的层次向上自动继承它的间接基类
🎆关于构造, 析构函数的执行顺序, 同理
标准模板库(STL)
1. string类
-
使用string类要包含头文件<string>
-
string对象的初始化方法
-
string s1;
-
string s1(“hello”);
-
string s1 = “hello”;
-
string s1(8, ‘x’); // 初始化为8个x字符的字符串
-
错误的初始化方法:
- string s1 = ‘n’;
- string s1(‘n’);
- string s1(6);
- string s1 = 6;
-
神奇: 可以将字符赋值给string对象
string s1; s1 = 'n';
-
-
用于获取string对象长度的成员函数: length()
-
string支持流读取
cin >> str;
- string支持getline函数
getline(cin, s);
- 用 = 号赋值
s2 = s1;
- 用assign成员函数复制
// 全复制
s2.assign(s1);
// 部分复制
s2.assign(s1, 1, 3); // 从s1的下标1开始的三个字符复制给s2
- 单个字符复制
s2[5] = s1[3] = 'n';
- 使用at成员函数访问string对象中的值
string s1("hello");
for(int i = 0; i < s1.length(); i++)
cout << s1.at(i) << endl;
at 和中括号的不同在于, at会做边界检查, 如果超出会报out_of_range异常
- 用+号连接字符串
string s1("good"), s2("morning");
s1 += s2; // good morning
- 用成员函数 append 连接字符串
// 全连接
s1.append(s2);
// 部分连接
s1.append(s2, 0, s2.size()); // 从0开始size个字符
如果没有足够的字符, 就直到连接完最后一个字符
- 可以用关系运算符(<, <=, >, >=, ==等)比较string的字典序
- 用成员函数compare比较string的字典序
// 比较全部
s1.compare(s2);
// 单方面部分比较
s1.compare(1, 2, s2); // 从1开始两个和s2比
// 双方面部分比较
s1.compare(1, 2, s2, 0, 2) // 用s1的从1开始2个, 和s2从0开始2个比
- 用成员函数 substr 获取string的子串
s2 = s1.substr(4, 5); // 将s1从4开始5个字符赋值给s2
- swap 交换内容
s1.swap(s2)
- find 从string中查找内容
string s1("hello");
// 从左到右查找
s1.find("lo");
// 从右往左查找
s1.rfind("lo");
// 从指定位置查找
s1.find("lo", 1); // 从下标1开始找
// 从左往右找其中一个
s1.find_first_of("abcd");
// 从右往左找其中一个
s1.find_last_of("abcd");
// 找不属于其中一个的
find_last_not_of, find_first_not_of
- erase 删除字符串中的元素
// 从指定位置开始删除
s1.erase(5) // 从5开始
- replace 取代字符串中的元素
// 用全部替换
s1.replace(2, 3, "hh"); // 将s1从2开始的3个字符替换成"hh"
// 用部分替换
s1.replace(2, 3, "haha", 1, 2); // 用"haha"中从1开始的2个来替换
- insert 向字符串中插入元素
string s1 = "hello";
string s2 = "ha";
// 全部插入
s1.insert(3, s2);
// 部分插入
s1.insert(3, s2, 0, 1); // 将s2中从0开始的1个字符插入
- c_str返回const char* 类型字符串
- data返回char* 类型字符串
- copy成员函数将自身复制给其他变量
string s1("hello world");
int len = s1.length();
char* p2 = new char[len+1];
s1.copy(p2, 5, 0); // 返回5
p2[5] = 0;
cout << p2 << endl;
-
字符串流处理
- 用istringstream类实例化流提取对象
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O6eSNYKM-1643544309020)(C:\Users\a\AppData\Roaming\Typora\typora-user-images\image-20220124154138330.png)]
- 用ostringstream类实例化
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zMUkvPCm-1643544309021)(C:\Users\a\AppData\Roaming\Typora\typora-user-images\image-20220124154355414.png)]
如果没有足够的字符, 就直到连接完最后一个字符
- 可以用关系运算符(<, <=, >, >=, ==等)比较string的字典序
- 用成员函数==compare==比较string的字典序
```cpp
// 比较全部
s1.compare(s2);
// 单方面部分比较
s1.compare(1, 2, s2); // 从1开始两个和s2比
// 双方面部分比较
s1.compare(1, 2, s2, 0, 2) // 用s1的从1开始2个, 和s2从0开始2个比
- 用成员函数 substr 获取string的子串
s2 = s1.substr(4, 5); // 将s1从4开始5个字符赋值给s2
- swap 交换内容
s1.swap(s2)
- find 从string中查找内容
string s1("hello");
// 从左到右查找
s1.find("lo");
// 从右往左查找
s1.rfind("lo");
// 从指定位置查找
s1.find("lo", 1); // 从下标1开始找
// 从左往右找其中一个
s1.find_first_of("abcd");
// 从右往左找其中一个
s1.find_last_of("abcd");
// 找不属于其中一个的
find_last_not_of, find_first_not_of
- erase 删除字符串中的元素
// 从指定位置开始删除
s1.erase(5) // 从5开始
- replace 取代字符串中的元素
// 用全部替换
s1.replace(2, 3, "hh"); // 将s1从2开始的3个字符替换成"hh"
// 用部分替换
s1.replace(2, 3, "haha", 1, 2); // 用"haha"中从1开始的2个来替换
- insert 向字符串中插入元素
string s1 = "hello";
string s2 = "ha";
// 全部插入
s1.insert(3, s2);
// 部分插入
s1.insert(3, s2, 0, 1); // 将s2中从0开始的1个字符插入
- c_str返回const char* 类型字符串
- data返回char* 类型字符串
- copy成员函数将自身复制给其他变量
string s1("hello world");
int len = s1.length();
char* p2 = new char[len+1];
s1.copy(p2, 5, 0); // 返回5
p2[5] = 0;
cout << p2 << endl;
-
字符串流处理
- 用istringstream类实例化流提取对象
[外链图片转存中…(img-O6eSNYKM-1643544309020)]
- 用ostringstream类实例化
[外链图片转存中…(img-zMUkvPCm-1643544309021)]