0
点赞
收藏
分享

微信扫一扫

C++学习笔记

简单聊育儿 2022-01-31 阅读 47

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. 而私有的, 我们只需要在内部函数加入一个判断即可
*/    
  • 成员函数的功能

    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, 可在外部定义
}
  • 一个函数可以有多个构造函数(不是重载) ==> 类型转换构造函数

    • 该函数和构造函数极其相似, 可以认为是特殊的构造函数

    • 如:

      1. Test a = 2; 这里只是调用了构造函数

      2. class Test{
            public:
            int val;
            Test(){}	//构造函数默认为空
            Test(int a){
                val = a;
            }
        }
        int main(){
            Test a;
            a = 2;		//这里就调用了类型转换构造函数
            a.getVal();
        
            return 0;
        }
        
      3. 🔥 类型转换构造函数本质是: 等号右边其他类型的值, 转化为一个临时对象, 并把该值赋给等号左边的对象, 之后会消亡

  • 如果没有写构造函数, 则编译器默认生成一个无参数的构造函数

  • 构造函数支持重载

Test::Test(){}
Test::Test(int a){}
Test::Test(int a, int b){}

构造函数被调用的几种情况(假设构造函数有参数)

  1. 实例对象

    • Test a(1); 正确

    • Test a; 错误, 没有加参数

    • Test a = 2; 正确

    • Test a; 🔥

      a = 2; 错误, a = 2并非初始化语句

  2. 定义类的指针

    • Test* p = new Test(1); 正确
    • Test* p = new Test; 错误
    • Test* p = new Test[2]; 错误, 但未知解决方法
    1. 对象数组
    • Test array[2] = {1, 2}; 正确
    • Test array[2] = {Test(1), Test(2)} 正确
    • Test array[2]; 错误, 没有参数
    1. 类指针数组
    • 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;
        }
    };

​ 因为, 这涉及到函数的传参机制与复制构造函数的关系. 如果采取该种方式, 当调用复制构造函数是, 首先要进行传参, 而传参又要调用复制构造函数, 如此反复 ==> 死递归

复制构造函数被调用的几种情况

  1. 当一个对象以另一个同类的对象值初始化时
Test a1(2);
Test a2(a1);	// 此时会调用复构函数
// 等价于	Test a2 = a1;	是初始化, 不是赋值语句哦!

易错:

Test a = 2;
Test b = 1;
b = a;	// 一个对象被另一个对象赋值时不会调用复制构造函数
  1. 当对象作为某函数的参数传参时
void func(Test a){}
Test a(2);
func(a);	// 此时会调用复构函数
  1. 当对象作为函数的返回值时, 会生成一个临时对象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");
    }
}

注意: 析构函数并不是销毁内存, 只是在销毁前做一些事情

对象消亡的几种情况

  1. 程序结束, 所有的变量销毁
  2. 作为局部变量的结束
  3. 调用函数生成的对象的结束

具体实例

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&

对运算符重载时, 好的习惯是保留运算符原有的特性

  1. 自身赋值给自身, 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;		// 这里又返回自身了
}
  1. 在调用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. 运算符重载的注意事项

  1. C++不允许定义新的运算符
  2. 重载后的运算符含义应符合日常习惯,如+表示加法的意思, 不是减法
  3. 运算符重载不改变运算符的优先级
  4. 以下运算符不能被重载, (" . ", " .* ", " :: ", " ?: ", " sizeof ")
  5. 重载运算符(), [], ->或者赋值运算符 = 时, 重载函数必须声明为类的成员函数

继承和组合

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_;
    }
};

构造, 析构函数调用顺序

​ 该顺序和封闭类相似

  1. 先调用基类的构造函数
  2. 如果有成员对象, 再调用成员对象的构造函数
  3. 最后调用派生类的构造函数

7. public继承的赋值规则

  1. 派生类对象可以赋值给基类对象

​ 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)]

举报

相关推荐

0 条评论