栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

C++学习笔记

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

C++学习笔记

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;


引用也可以引用 " 引用 "

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的使用

在C语言中, 我们使用malloc函数来动态分配内存, 而在C++中, 我们使用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

这两种new 的返回结果都是 ElemType * 类型

2. delete的使用

当动态分配的内存不需要的时候, 我们可以使用delete来释放内存

释放单个变量空间

delete p;

释放一片连续内存空间

delete[] p_to_arr;
delete p_to_arr;	// 这样是错的, 只释放了单个空间

delete 不能用来释放不属于动态分配的内存空间

内联函数, 重载函数⚔️ 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. 函数的缺省参数

类似于Python的函数默认值, 如果在定义函数时为函数参数赋值, 则该参数就有了默认值, 在调用时可以省略.

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);
        }
};		// 记得加分号

​ 另一种方式: 在类的外部定义函数

class CRectangle{
   public:
   int h, w;
   void set_val(int, int);
   int calArea();
   int calPeri();
};
void CRectangle::set_val(int a, int b){}
int CRectangle::calArea(){}
int CRectangle::calPeri(){}
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;

这些就是c++中类的基本使用操作了

4. 修饰符

private: 私有属性, 只能通过内部方法来访问public: 公有属性, 可以直接访问protected: 保护属性, 目前未知

​ private私有属性的好处在于: 避免了随意访问该变量, 当要修改时需要全部改动, 而如果是私有的, 只能通过内部函数来修改, 只用修改内部函数即可

// 公有的szName
strcpy(Person.szName, "Tom123456789")
// 私有的
Person person;
person.setName("Tom123456789")
// 假设我们修改内部属性szName长度为5, Tom123...显然越界
    

成员函数的功能

    可以访问当前对象的全部属性, 函数也可以访问同类的其他对象的全部属性, 函数
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::Test( Test x )呢?

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

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

    当一个对象以另一个同类的对象值初始化时
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);	// 此时会调用复构函数

有时为加快程序的运行速度将参数设为Test& a,就不用传参了, 也可加上const

    当对象作为函数的返回值时, 会生成一个临时对象func()
Test func(int a){
    Test x(a);
    return x;
}
cout << func(3).val << endl;

注意: 在dev C++中, 函数返回值并不会调用复制构造函数, 而是直接返回

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D2LzIcec-1643544309015)(C:UsersaAppDataRoamingTyporatypora-user-imagesimage-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的消亡
}

特例: 当动态申请(new)了某个内存却不销毁时, 程序结束它也不会消亡

8. this指针

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并没有指向任何对象, 只是一个该类类型的空指针

对于静态成员函数, 就不可以使用this指针, 因为静态成员函数不作用于某个对象

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. 友元

友元是朋友的意思, 使用friend关键字声明

​ 友元分为 友元函数 和 友元类 两种

友元函数: 可以是类的成员函数(包括构造函数, 析构函数), 也可以是全局函数

一个类的友元函数可以访问该类的私有成员

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 运算符符号 (参数表) { }

使用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;		// 返回自身
}

其实到复制完后, 我们的目的已经达到了, 那么为什么要返回自身呢, 类型还是String&

被同类对象赋值

​ 对象之间直接赋值一般都是没有问题的, 但是对于该String类, 就问题了

​ 问题就是: A = B;

​ 用来赋值的B的成员是new出来的一片空间, 对于普通的赋值, 会让指针指向同一片空间

坏处:

当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&

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

上述对于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, cin搭配, 分别重载为流插入, 和流提取运算符

cout 是在ostream类的对象, cin即istream类的对象, 可以直接调用的

下面以**cout 输出 5 / “hello”**为例:

重载为ostream的成员函数

void ostream::operator<< (int n) {}

前面我们提到了, 对运算符的重载要尽量保留其原有特性

如: cout << 5 << “hello”; 我们应如何实现?

答案很简单, 就是返回cout本身, 即返回值为ostream&, 就可以叠加运算了

ostream& ostream::operator<< (int n) {}

cout << 5 << “hello” 的本质就是 => cout.operator<<(5).operator<<(“hello”);

重载为普通函数

​ 在平常我们自己需要对对象进行一些输入, 输出的操作时, 我们肯定不能去修改ostream类的成员函数, 因此我们需要将其重载为全局函数

如下例:

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. 类型转换运算符的重载

当在需要将对象转为xx类型的时候, 会自动调用类型转换重载函数

定义形式:

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;	// 返回临时对象
}

​ 从观察前置, 后置的定义可知, 在仅需要自增时, ++i会快很多

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

​ 在定义一个新的类B时, 如果该类和类A相似(指的是B拥有A的全部特点), 就可以将A作为基类, B作为A的派生类, 这种关系叫做类的继承

继承的使用形式: 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类

组合

​ 有时候, 继承不一定适合任何情况.

​ 当该类具有A类的特点, 又具有B类的特点, 就可以将其作为成员组合起来, 形成一个新的类

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:UsersaAppDataRoamingTyporatypora-user-imagesimage-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继承的赋值规则

继承public基类的派生类可实现的操作

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

​ b = d;

2. 派生类对象可以初始化基类对象的引用

​ base& br = d;

3. 派生类对象的地址可以赋值给基类对象的指针

​ base* pb = &d;

只能通过该指针访问派生类中继承基类的成员可以通过强制类型转化, 将 基类指针变量 类型转化为 派生类指针变量 类型

如果是通过private / protected方式继承的, 则不可以实现

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HSpPzPB7-1643544309019)(C:UsersaAppDataRoamingTyporatypora-user-imagesimage-20220129194742876.png)]

8. 多重继承

B类是继承A类的派生类, C类是继承B类的派生类, C类就是A类多重继承产生的

类A是类B的直接基类类A是类C的间接基类类B是类C的直接基类

在定义派生类时, 只需要列出其直接基类, 派生类会沿着类的层次向上自动继承它的间接基类

关于构造, 析构函数的执行顺序, 同理

标准模板库(STL) 1. 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

找到了就返回第一次出现的下标, 没有就返回静态常量string::npos;

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;

将s1从0开始最长为5个字符复制给p2, 返回真实的复制字符个数

字符串流处理

用istringstream类实例化流提取对象

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O6eSNYKM-1643544309020)(C:UsersaAppDataRoamingTyporatypora-user-imagesimage-20220124154138330.png)]

用ostringstream类实例化

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zMUkvPCm-1643544309021)(C:UsersaAppDataRoamingTyporatypora-user-imagesimage-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

找到了就返回第一次出现的下标, 没有就返回静态常量string::npos;

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;

将s1从0开始最长为5个字符复制给p2, 返回真实的复制字符个数

字符串流处理

用istringstream类实例化流提取对象

[外链图片转存中…(img-O6eSNYKM-1643544309020)]

用ostringstream类实例化

[外链图片转存中…(img-zMUkvPCm-1643544309021)]

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/723256.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号