C++学习笔记

欢迎关注我的微信公众号【万能的小江江】

from:C++多文件编程

暑假的时候学习了一段时间C++,但是因为用的比较少,很多细节都已经忘记。所以还是要在学习中实践,在实践中学习~

第1章:从C到C++

system("cls"); //清屏
xxx == 1 ?"A":"B"; //如果xxx是1返回A否则B  

int *p = (int*) malloc(sizeof(int) * 10); //分配10个int型的内存空间
free(p); //释放内存

int *p = new int[10]; //分配1个int型的内存空间
delete[] p; //释放内存

malloc()和new都是在堆区分配内存,必须手动释放,否则只能等到程序运行结束由操作系统回收

为避免内存泄露,通常new和delete、new[]和delete[]操作符应该成对出现,并且不要和C语言中的malloc()、free()混用

C++中最好用new和delete来管理内存,因为可以用到C++中一些新特性,比如自动调用构造函数和析构函数

内联函数inline

函数的调用有时间和空间的开销,在执行函数的时候用内联函数,可以在编译的时候把函数用函数体来代换(类似C语言中的宏展开)

这又被称为内嵌函数或内置函数,缺点是编译后的程序会存在多分相同的函数拷贝,编译后程序的体积会变大,一般只将短小的、频繁调用的函数声明为内联函数

简单来说就是消除函数调用的时空开销

内联函数只是对编译器的一个建议,并不是强制性的,编译器会自己判断决定是否这样做

内联函数不应该有声明,而是把函数定义在本该出现函数声明的地方,这是一种良好的编程风格

将内联函数的声明和定义分散到不同文件中链接会出错(因为内联函数编译完就不存在了)

多文件编程时,建议将内联函数的定义直接放在头文件中,并禁用内联函数声明(声明是多此一举)

inline void swap(int *a,int *b)
{

} //内联函数的使用方法,调用的时候不需要加inline,正常调用就行

内联函数和宏的区别

内联函数可以定义在头文件中(不用加static关键字),非内联函数不行(内联函数在编译时会将函数调用处用函数体替换,编译完成后函数就不存在了,所以在链接时不会引发重复定义的错误)

宏是在预处理时被展开,编译时就不存在了

内联函数更像是编译期间的宏

内联函数的作用

  1. 消除函数调用时的开销
  2. 取代带参数的宏(更能凸显意义)

C++函数的默认参数

默认参数就是给函数的形参设定一个默认值,如果没有实参的时候,就自动使用这个默认值

需要注意的是,如果给某个形参指定了默认值,那它后面所有的形参都必须有默认值(实参和形参的传值是从左到右依次匹配的,默认参数的连续性是保证正确传参的前提)

通过使用默认参数,可以减少要定义的析构函、方法以及方法重载的数量(不太懂)

C++形参在同一作用范围中只能指定一次默认参数,如果在同一个cpp文件函数定义中已经指定其中几个默认参数,在调用的时候再指定另外一个默认参数,就会报错(放在不同文件就不会,这个部分比较难通过文字理解)

C++函数的重载

  • 在C语言中,如果变量有多种类型,我们要把不同的变量换入函数内部需要设计几个不同名的函数

  • 在C++中,利用函数的重载,可以让一个函数名有多种用途(传入不同类型的参数不用重新定义函数,用原来的函数就可以)

  • C++允许多个函数拥有相同的名字,只要它们的参数列表不同就可以,这就是函数的重载(Function Overloading)

  • 参数列表又叫参数签名,包括参数的类型、参数的个数和参数的顺序,只要有一个不同就叫做参数列表不同

  • 参数列表不同包括参数的个数不同、类型不同或顺序不同,仅仅参数名称不同是不可以的。函数的返回值也不能作为重载的依据

  • 重载就是在一个作用范围内(同一个类、同一个命名空间)有多个名称相同但参数不同的文件)重载让一个函数名拥有了多种用途,使得命名更加方便、调用更加灵活

  • 不过也要注意的是,使用重载函数时,同名函数的功能应该相同或相近

函数重载的规则

  1. 函数名称必须相同
  2. 参数列表必须不同(个数不同、类型不同、参数排列顺序不同等)
  3. 函数的返回类型可以相同也可以不同
  4. 仅仅返回类型不同不足以成为函数的重载

重载是怎样进行的

C++在编译的时候会根据参数列表对函数重命名,比如void Swap(int a,int b)会被重命名为_Swap_int_intvoid Swap(float x,float y)会被重命名为_Swap_float_float

发生函数调用的时候,编译器会根据传入的实参去逐个匹配,以选择对应的函数,如果匹配失败(实参和形参类型不一样),编译器会报错,这就叫做重载决议(Overload Resolution)

所以,从本质上来说,函数的重载只是在语法层面的,它们还是不同的函数,占用不同的内存,入口地址也不一样

C++函数重载的二义性和类型转换

类型提升

为了更加高效地利用计算机硬件,不会导致数据丢失或精度降低

  • 当实参和形参类型不一致的时候,编译器会自动匹配,转换实参类型。如果发生调用错误,不知道如何抉择的时候按照下面的优先级来处理实参的类型

C++重载决议

类型转换

是不得已而为之,不能保证数据的正确性,也不能保证应有的精度。除了上表所列的几种情况,其他情况都是类型转换

多个参数时的二义性

重载函数有多个参数的时候会产生二义性,且情况十分复杂。C++标准规定,如果有且只有一个函数满足下列条件,则匹配成功:

  • 该函数对每个实参的匹配都不劣于其他参数
  • 至少有一个实参的匹配优于其他函数

如何实现C++和C混合编程(需要复习)

C无法实现函数重载,需要用extern"C"

extern“C”的功能是让编译器以处理C语言代码的方式来处理修饰C++的代码

extern”C”的2种用法

  1. 只修饰一句C++代码时,直接将其添加到该函数代码的开头
  2. 用于修饰一段C++代码是,只需要为extern”C”添加一对大括号{},并将要修饰的代码囊括到括号里

第2章:类和对象

C++的类是一种构造类型,相对于结构体来说,类的成员可以是变量也可以是函数,通过类定义出来的变量叫做“对象”

类只是一个模板(Template)编译后不占用内存空间,所以在定义类的时候不能对成员变量进行(赋值)初始化,因为没有地方存储数据(只有创建对象之后才会给成员变量分配内存,这时候才可以赋值)

class Student{
public:
    //成员变量
    char *name;
    int age;
    float score;

    //成员函数(结构体不能包含函数,类可以)
    void say(){
        cout<

使用对象指针

new是在上创建对象,通过new创建出来的对象只有一个指向它的指针,没有名字。所以要用一个指针变量来接受这个指针,否则以后就找不到这个对象,也没法使用这个对象了

通过对象名字访问成员用点号.,通过对象指针访问成员用箭头->

//对象名字,在栈上创建对象,形式与定义普通变量类似
Student LiLei;
LiLei.age;
//对象指针,在堆上使用new关键字创建对象,必须使用一个指针指向它,用完要delete不用的对象
Student &pStu = new Student;
pStu -> name = ;

栈和堆的区别

栈(Stack)的内存是程序自动管理的,不能由我们操作,不能用delete删除栈(Heap)上创建的对象。堆由程序员管理,可以用new创建对象,对象使用完毕后可以通过delete删除。实际开发中 ,new和delete一般成对出现,以保证及时删除不再使用的对象,防止无用的内存堆积

C++类的成员变量和成员函数详解

在类的外面定义类的函数,就要在函数名前面加上类名限定::为域解析符(也称为域作用域运算符或作用域限定符),用来连接类名和函数名,指明当前函数属于哪个类

成员函数必须先在类体中作原型声明,然后在类外定义,类体的位置应该在函数定义之前

在类体中定义的成员函数会自动成为内联函数,在类体外定义的就不会(可以通过加inline关键字定义为内联函数,在声明的地方加inline没有用)

class Student{
public:
    //成员变量
    char *name;
    int age;
    float score;

    void say();
}
//成员函数(结构体不能包含函数,类可以)
void Student::say(){
    cout<

C++类成员的访问权限及类的封装

  • C++通过public公有的、protected受保护的、private私有的三个关键字来控制成员变量和成员函数的访问权限

  • 和Java、C#不同,C++中的public、private、protected只能修饰类的成员,不能修饰类

  • 在类的内部,不论权限如何都可以互相访问

  • 在类的外部,只能通过对象访问public属性的成员

  • 成员变量一般习惯用m_开头,可以和成员函数中的形参名字区分开

类的封装

  • 给成员变量赋值的函数通常称为set函数,名字通常以set开头
  • 读取成员变量的值的函数通常称为名字通常以get开头
  • 封装就是尽量隐藏类的内部实现,只向用户提供有用的成员函数
  • 在一个类体中,private和public可以分别出现多次,但是尽量都只出现一次会比较好

C++对象的内存模型

  • 类是创建对象的模板,不占用内存的空间。创建对象后,才需要在堆区或者栈区分配内存

  • 编译器会把成员变量和成员函数分开存储,每个对象的成员变量都会被分配内存,但是所有对象都共享同一段函数代码

  • 成员变量在堆区或者栈区分配内存,成员函数在代码区分配内存

举例

用sizeof获取对象所占内存大小

#include 
using namespace std;

class Student{
    private:
        char *m_name;
        int m_age;
        float m_score;
    public;
        void setname(char *name);
        void setage(int age);
        void setscore(float score);
        void show();
}

void Student::setname(char *name){
    m_name = name;
}

void Student::setname(int age){
    m_age = age;
}

void Student::setscore(float score){
    m_score = score;
}

void Student::show(){
    cout << m_name  << "的年龄是:" << m_age << ",成绩是:" << m_score << endl;
}

int main(){
    //在栈上创建对象
    Student stu;
    cout << sizeof(stu) << endl;
    //在堆上创建对象
    Student *pstu = new Student();
    cout << sizeof(*pstu) << endl;
    //类的大小
    cout << sizeof(Student) << endl;

    return 0;
}

//运行结果
12
12
12
  • Student类包含3个成员变量,类型分别是char ,int,float,每个都占用4字节内存,加起来共占用12字节的内存,用sizeof求得结果为12,*说明对象所占用的内存仅包含成员变量

  • 假设stu的起始地址为0x1000,则该对象内存分布如下图

    和结构体相似,也会有内存对齐的问题

C++函数编译原理和成员函数的实现

由上节可知,对象的内存中只保留了成员变量,那么其他四个成员函数是怎么通过对象调用的呢?

C++函数的编译

C语言编译方式

C语言中函数在编译的时候名字不变,或者只是简单加个下划线(看编译器)

func()编译后为func()_func()

C++编译方式
  • C++在编译的时候会根据其命名空间、所属的类、参数列表(或叫参数签名)来重新命名,形成一个新的函数名。新的函数名只有编译器知道,对用户是不可见的

  • 这个重命名的过程叫做名字编码(Name Mangling),通过一种特殊的算法来实现

  • 这种算法是可逆的,可以确保新函数名的唯一性。可以只声明而不定义函数,就可以在报错信息中看到新函数名

    VS2010报错信息

成员函数的调用

  • 成员函数最终都会被编译成与对象无关的全局函数,如果函数体中没有成员变量,那就直接调用就可以
  • 如果使用了成员变量,因为成员变量的作用域不是全局,所以编译成员函数要额外添加一个参数,把当前对象的指针传递进去,通过指针来访问成员变量
举例(有点不太理解)

假设Demo类有两个int类型的成员变量,分别是a和b,并且都在display()中用到了

void Demo::display(){
    cout << a << endl;
    cout << b << endl;
}

编译后的代码类似于:

void new_function_name(Demo *const p){
    //通过指针p来访问a、b
    cout << p->a << endl;
    cout << p->b << endl;
}

使用obj.display()调用函数时,也会被编译成类似下面的形式

new_function_name(&obj); 

这样通过传递对象指针就完成了成员函数和成员变量的关联,通过对象调用成员函数的时候是通过函数找对象

这一切都是隐式完成的,对程序员来说完全透明。Demo * const p中的const表示指针不能被修改,p只能指向当前对象,不能指向其他对象

C++构造函数详解

构造函数(Constructor)

  • 构造函数必须是public属性的,否则无法调用
  • 名字和类名相同
  • 没有返回值
  • 不需要用户显示调用(用户也不能调用)
  • 在创建对象时自动执行

析构函数存在的意义就是为了简化我们之前提到的用setname()这样的成员函数去给private里面的成员变量赋值的过程,可以在创建对象的同时为成员变量赋值

析构函数使用

因为析构函数没有返回值,所以不需要变量来接收返回值

  • 不论是声明还是定义,函数名前面都不能出现返回值类型,void也不允许
  • 函数体中不能有return语句
#include 
using namespace std;

class Student{
    private:
        char *m_name;
        int m_age;
        float m_score;
    public:
        //声明构造函数
        Student(char *name,int age,float score);
        //声明普通成员函数
        void show();
}

//定义构造函数
Student::Student(char *name,int age,float score){
    m_name = name;
    m_age = age;
    m_score = score;
}

//定义普通成员函数
void Student::show(){
    cout << m_name << "的年龄是:" << m_age << ",成绩是:" << m_score << endl;
}

int main(){
    //创建对象时向构造函数传参
    Student stu("小明",15,92.5f);
    stu.show();
    //创建对象时向构造函数传参
    Student *pstu = new Student("李华",16,96);
    pstu -> show();

    return 0;
}

构造函数的重载

  • 构造函数的调用是强制性的,一旦在类中定义了构造函数,创建对象的时候就一定要调用,如果不调用就会报错
  • 在栈上创建对象Student stu("小明",15,92.5f)
  • 在堆上创建对象new Student("李华",16,96)

默认构造函数

用户自己没有定义构造函数的话编译器会自动生成一个默认的构造函数,只是这个构造函数的函数体是空的没有形参,也不执行任何操作。就比如上面的Student类,默认生成的构造函数如下

Student(){}
  • 类必须有构造函数,要么用户自定义,要么编译器自动生成。一旦用户自定义,编译器就不再自动生成

  • 调用没有参数的构造函数可以省略括号,在栈上创建对象可以写成Student stu()Student stu,在堆上创建对象同理

C++构造函数初始化列表(有点不太会)

//采用初始化列表
Student::Student(char *name,int age,float score):m_name(name),m_age(age),m_score(score{
//TODO;
}
  • 这句语句的意思相当于函数体内部的m_name = name;m_age = age;m_score = score;语句,也是赋值的意思
  • 构造函数初始化列表并没有效率上的优势,仅仅只是书写方便,尤其当成员变量变多的时候,这种写法比较简单明了
Student::Student(char *name;int age;float score):m_name(name){
    m_age = age;
    m_score = score;
}
//成员变量的初始化顺序和初始化列表列出的变量顺序无关,只与成员变量在类中声明的顺序有关(意思是不管你怎么列出来,都是按照成员变量在类中声明的顺序来定)

初始化const成员变量

  • 初始化const成员变量唯一的方法就是使用初始化列表
class VLA{
    private:
        const int m_len;
        int *m_arr;
    public:
        VLA(int len);
}

//必须使用初始化列表来初始化m_len
VLA::CLA(int len):m_len(len){
    m_arr = new int[len];
}

//另外一种写法
VLA::CLA(int len){
    m_len = len;
    m_arr = new int[len];
}

C++析构函数详解

  • 创建对象系统会自动调用构造函数进行初始化工作,销毁对象时系统也会自动调用一个函数来清理工作(比如释放分配的内存,关闭打开的文件等)
  • 析构函数(Destructor)也是一种特殊的成员函数,没有返回值,不需要也不能被显式调用,在销毁对象时自动执行
  • 析构函数没有参数,不能被重载,一个类只能有一个析构函数(用户没有定义,编译器也会默认生成一个析构函数)
//部分代码
class VLA{
    public:
        VLA(int len); //构造函数
        ~VLA(); //析构函数
   ...
    private:
        int *at(int i); //获取第i个元素的指针
    private:
        const int m_len; //数组长度
}

VLA::~VLA(){
    delete[] m_arr; //释放内存
}

int main(){
    VLA *parr = new  VLA(n);
    ...
    delete parr; //删除数组/对象
}

//~VLA()作用就是VLA类的析构函数,它的唯一作用就是删除对象后释放已经分配的内存
  • 原则上函数名的标识符不能用~,析构函数是特例
  • at()函数只能在类的内部使用,所以声明为private属性
  • m_len变量不允许修改,所以用const进行限制,这样就只能用初始化列表来赋值了

new和delete与malloc free最大的区别

  • new分配内存会调用构造函数,delete释放内存会调用析构函数
  • 构造函数和析构函数对类来说是不可或缺的

析构函数的执行时机

  • 析构函数在对象被销毁的时候调用,对象销毁的时间和其所在的内存区域相关(在C语言部分第14章有内存相关内容)
  • 在所有函数之外创建的对象是全局对象,和全局变量类似,位于内存中的全局数据区,程序在结束执行的时候会调用这些对象的析构函数
  • 在函数内部创建的对象是局部对象,他和局部变量类似,位于栈区,函数执行结束时会调用这些对象的析构函数
  • new创建的对象位于堆区。要通过delete删除时,才会调用析构函数;如果没有delete,析构函数就不会被执行
//析构函数的执行
#include 
#include 
using namespace std;

class Demo{
    public:
        Demo(string s);
        ~Demo();
    private:
        string m_s;
};
Demo::Demo(string s):m_s(s){}
Demo::~Demo(){cout<

C++对象数组(数组的每个元素都是对象)

C++允许数组的每个元素都是对象,这样的数组称为对象数组

对象数组中每个元素都需要用构造函数初始化,具体哪些元素用哪些函数初始化,需要取决于定义数组时的写法,如下面的例子:

#include
using namespace std;

class CSample{
    public:
        CSample(){//构造函数1,无参构造函数初始化
        cout << "Constructor 1 Called" << endl;
        }
        CSample(int n){//构造函数2
        cout << "Constructor 2 Called" << endl;
        }
};

int main(){
    cout << "step1" << endl;
    CSample array[2];//这2个元素没有指明如何初始化,默认就调用无参构造函数初始化

    cout << "step2" << endl;
    CSample array2[2] = {4,5};//初始化列表{4,5}可以视作初始化两个数组元素的参数。array2[0]以4为参数,array2[1]以5为参数,所以构造函数2进行初始化

    cout << "step3" << endl;
    CSample array3[2] = {3};//它只指出了array3[0]的初始化方式,array3[1]没有指出,所以还是按照构造函数2和构造函数1进行初始化

    cout << "step4" << endl;
    CSample* array4 = new CSample[2];//动态分配了一个CSample数组,其中有2个元素。但是没有指出和参数有关的信息,所以都按照无参处理
    delete [] array4;

    return 0;
}

运行结果:

step1
Constructor 1 Called
Constructor 1 Called
step2
Constructor 2 Called
Constructor 2 Called
step3
Constructor 2 Called
Constructor 1 Called
step4
Constructor 1 Called
Constructor 1 Called

当构造函数有多个参数时,数组的初始化要显式包含对构造函数的调用

class CTest{
    public:
        CTest(int n){} //构造函数1
        CTest(int n,int m){} //构造函数2
        CTest(){} //构造函数3
};
int main(){
    //三个元素分别用构造函数1 2 3 初始化
    CTest array[3] = {1,CTest(1,2)};
    //三个元素分别用构造函数2、2、1初始化
    CTest array2[3] = {CTest(2,3),CTest(1,2),1};
    //两个元素指向的对象分别用构造函数1、2初始化
    CTest* pArray[3] = {new CTest(4),new CTest(1.2)};//pArray数组是一个指针数组,元素不是CTest类的对象,而是CTest指针。此处只对pArray[0]和pArray[1]进行了初始化,pArray[2]是随机的,不知道指向哪里。因为只生成了2个CTest对象,所以只调用了2次CTest类的构造函数
    return 0;
}

C++成员对象和封闭类详解

一个类的成员变量如果是另外一个类的对象,就称为“成员对象”,包含成员对象的类叫封闭类(enclosed class)

成员对象的初始化(比较重要)

创建封闭类的对象时,它包含的成员对象也要被创建,就会引发成员对象构造函数的调用。去了解C++构造函数初始化列表,可以了解编译器知道成员对象是如何知道which构造函数初始化的

构造函数初始化列表的写法
类名::构造函数名(参数表):成员变量1(参数表):成员变量2(参数表,...
{
    //TODO:
}
  • 对于基本类型的成员变量,”参数表“中只有一个值,就是初始值。在调用构造函数的时候会把这个值直接赋给成员变量
  • 对于成员对象,”参数表“中存放的是构造函数的参数,它可能是一个值,也可能是多个值,指明了该对象如何被初始化
例子(自己码一遍)
#include 
using namespace std;

//轮胎类
class Tyre{
    public:
        Tyre(int radius,int width);
        void show() const;
    private:
        int m_radius;//半径
        int m_width;//宽度
};
Tyre::Tyre(int radius,int width):m_radius(radius),m_width(width){}
void Tyre::show() const{
    cout << "轮毂半径:" << this->m_radius << "吋" << endl;
    cout << "轮胎宽度:" << this->m_width <<"mm"<< endl;
}

//引擎类
class Engine{
    public:
        Engine(float displacement = 2.0);
        void show() const;
    private:
        float m_displacement;
};
Engine::Engine(float displacement) : m_displacement(displacement){}
void Engine::show() const{
    cout << "排量:" << this->m_displacement << "L" << endl;
}

//汽车类
class Car{
    public:
        Car(int price,int radius,int width);
        void show() const;
    private:
        int m_price;//价格
        Tyre m_tyre;
        Engine m_engine;
};
Car::Car(int price,int radius,int width):m_price(price),m_tyre(radius,width)/*指明m_type对象的初始化方式*/{};
void Car::show() const{
    cout << "价格:" << this->m_price << "¥" << endl;
    this->m_tyre.show();
    this->m_engine.show();
}

int main()
{
    Car car(200000,19,246);//在这一行,编译器需要知道car对象中的m_tyre和m_engine怎么初始化
    car.show();
    return 0;
}

//Result
价格:200000¥
轮毂直径:19吋
轮胎宽度:245mm
排量:2L
  • Car就是一个封闭类,有两个成员对象:m_tyre.show()m_engine.show()
  • Car::Car(int price,int radius,int width):m_price(price),m_tyre(radius,width)/*指明m_type对象的初始化方式*/{};这时候编译器已经知道car对象是用Car(int price,int radius,int width)这个构造函数进行初始化的
    • m_tyre应以radiuswidth作为参数调用Tyre(int radius,int width)构造函数进行初始化
    • 这里没有说明m_engine该如何处理,编译器会默认m_engine应该用Engine类无参构造函数进行初始化。Engine类刚好有个无参构造函数(设置了默认参数因为),所以整个car对象的初始化问题就解决了
  • 生成封闭类对象的语句一定要让编译器明白成员对象是如何初始化的,否则就会编译错误!如果Car类没有初始化列表Car car(200000,19,246)就会编译出错,因为编译器不知道如何初始化car.m_tyre,因为Tyre类没有无参构造函数,编译器又找不到初始化car.m_type对象的参数

成员对象的消亡

  • 封闭类对象生成的时候

    • 先执行所有成员对象的构造函数,之后才执行封闭类自己的构造函数
    • 成员对象构造函数的执行次序和成员对象在类的定义中的次序一致,它们只有在构造函数初始化列表中才和出现的次序无关
  • 封闭类对象消亡的时候

    • 先执行封闭类的析构函数,之后再执行成员对象的析构函数
    • 成员对象析构函数的执行次序和构造函数的执行次序相反,先构造后析构(这是C++处理此类次序问题的一般规律)
    #include
    using namespace std;
    
    class Tyre{
        public:
            Tyre() { cout << "Tyre constuctor" << endl; } 
               ~Tyre() { cout << "Tyre destructor" << endl;}
    };
    
    class Engine{
        public:
            Engine() { cout << "Engine constuctor" << endl; }
            ~Engine() { cout << "Engine destructor" << endl; }
    }
    
    class Car{
        private:
            Engine engine;
            Tyre tyre;
        public:
            Car() { cout << "Car constuctor" << endl; }
            Car() { cout << "Car destructor" << endl; }
    }
    
    int main(){
        Car car;
        return 0;
    }
    //Result
    Engine constuctor
    Tyre constuctor
    Car constuctor
    Car destructor
    Tyre destructor
    Engine destructor

C++ this指针详解(精辟)

  • this是C++中的一个关键字,是一个const指针**,指向当前对象**(通过它可以访问当前对象的所有成员)
  • 当前对象指的就是当前正在使用的对象,比如stu.show()stu就是当前对象,this指向stu

例子

#include
using namespace std;

class Student{
    public:
        void setname(char *name);
        void setage(int age);
        void setscore(float score);
        void show();
    private:
        char *name;
        int age;
        float score;
}

void Student::setnanme(char *name){
    this->name = name;
}

void Student::setage(int age){
    this->age = age;
}

void Student::setscore(float score){
    this->score = score;
}

void Student::show(){
    cout << this->name << "的年龄是"  << this->age << ",成绩是" << this->score << endl;
}

int main(){
    Student *pstu = new Student;
    pstu -> setname("李华");
    pstu -> setage(16);
    pstu -> setscore(96.5);
    pstu -> show();

    return 0;
}
  • this只能用在类的内部,通过this可以访问类的所有成员,包括public、private、protected
  • 这个例子中成员函数的参数和成员变量重名,只能通过this区分
  • this是一个指针,要用->来访问成员变量或成员函数
  • 用户不能显式给this赋值,只有对象创建后编译器才会自动给this。本例中,this的值和pstu的值是相同的

添加printThis()

void Student::printThis(){
    cout << this << endl;
}

在main()函数中创建对象并调用printThis():

Student *pstu1 = new Student;
pstul->printThis();
cout  << pstul << endl;

Stduent *pstu2 = new Student;
pstu2->printThis();
cout << pstu2 << endl;

//Result
0x7b17b8
0x7b17b8
0x7b17f0
0x7b17f0
  • this是const指针,值是不能被修改的。对于该指针赋值、递增、递减都会报错
  • this只能在成员函数内部使用,在其他地方使用也是非法的
  • 只有对象被创建后this才有意义,不能在static成员函数中使用(后续会提到)

this到底是什么

  • this是成员函数的形参,在调用成员函数的时候对象的地址作为实参传给this。this这个形参是隐式的,不出现在代码中,而是编译阶段由编译器默默将其添加到参数列表中
  • this作为隐式形参本质上是成员函数的局部变量,只能用在成员函数的内部,通过对象调用成员函数的时候才给this赋值
  • (需要review)见“C++函数编译原理和成员函数的实现“这一节欸,成员函数最终会被编译成与对象无关的普通函数(就是一大串包含各种信息的那种函数),除了成员变量会丢失所有信息,所以在编译的时候要在成员函数中添加额外的参数把当前对象的首地址传进去,来关联成员函数和成员变量。这个额外的参数,就是this

C++ static静态成员变量

初始化static静态成员变量

  • 对象的内存中包含了成员变量,不同的对象占用不同的内存(在”c++对象的内存模型”中有提到),这使得不同对象的成员变量相互独立,它们的值不受其他对象的影响(有两个相同类型的对象a、b,都有一个成员变量m_name,那么修改a.m_name的值不会影响b.m_name

  • 如果在多个对象之间共享数据,对象a改变了某份数据后对象b可以检测到(以之前提到的Student类为例,如果想知道班级中有多少学生,就可以设置一份共享的变量,每次创建对象后让该变量加1)(简单来说,就是创建新的类的时候,所有成员变量都是新的,独占的。我们创建static的目的就是为了在这些成员变量中独立出来,不受新创建对象时候的影响)

  • 在C++中,我们可以修改静态成员变量来实现多个对象共享数据的目标。静态成员变量是一种特殊的成员变量,它被关键字static修饰

  class Student{
      public:
          Student(char *name,int age,float score);
          void show();
      public:
          static int m_total;//静态成员变量
      private:
          char *m_name;
          int m_age;
          float m_score;
  };
  • 这段代码声明了一个静态成员变量m_total,用来统计学生的人数
  • static成员变量属于类,不属于某个具体的对象,即使创建了多个对象,也只为m_total分配一份内存,所有成员对象都使用这份内存中的数据。当某个对象修改了m_total,也会影响到其他对象
  • static成员变量必须在类声明的外部进行初始化,具体形式:

    type class::name = value;
  • type是变量的类型,class是类名,name是变量名,value是初始值。比如,将上面的m_total初始化

    int Student::m_total = 0;
  • 静态成员变量在初始化的时候不能再加static,但必须要有数据类型。被private、protected、public修饰的静态成员变量都可以用这种方式初始化

  • static成员变量的内存是在类外初始化的时候才分配,没有在类外初始化的static成员变量不能使用

访问static成员变量

//通过类访问static成员变量
Student::m_total = 10;
//通过对象访问static成员变量
Student stu("小明",15,92.5f);
stu.m_total = 20;
//通过对象指针来访问static成员变量
Student *pstu = new Student("李华",16,96);
pstu -> m_total = 20;
  • 三种方法是等效的

  • static成员变量不占用对象的内存,而是在对象外创建内存,所以不创建对象也可以访问static

    static成员变量和普通的static变量类似,都是在内存分区中的全局数据区分配内存

    全局、局部、new占用内存分区

例子

#include 
using namespace std;

class Student{
    public:
        Student(char *name,int age,float score);
        void show();
    private:
        static int m_total;//静态成员变量
    private:
        char *m_name;
        int m_age;
        float m_score;
};

//初始化静态成员变量
int Student::m_total = 0;
Student::Student(char *name,int age,float score):m_name(name),m_age(age),m_score(score){
    m_total++;//操作静态成员变量
}

void Studeng::show(){
    cout << m_name << "的年龄是" << m_age << ",成绩是" << "m_score" << "(当前共有" << m_total << "名学生)" << end;

int main(){
    //创建匿名对象
    (new Student("小明",15,90)) -> show();
    (new Student("李磊",16,80)) -> show();
    (new Student("小明",16,99)) -> show();
    (new Student("小明",14,60)) -> show();

    return 0;
}

result

  • 本例将m_total声明为静态成员变量,每次调用的时候,会调用构造函数使m_total的值加1
  • 之所以叫匿名对象,是因为每次创建对象后只会使用其的show函数,不再进行其他操作。but匿名对象无法回收内存,会导致内存泄露,在中大型程序中最好不要用。

几点说明

  • 一个类中可以有一个或多个静态成员变量,所以对象都可以共享,都可以引用

  • static成员变量和普通static变量一样,都在内存分区中的全局数据区分配内存,到程序结束时才释放(所以static成员变量不随对象的创建而分配内存,也不随对象的销毁而释放内存,与普通变量不同)

  • 静态成员变量必须初始化,且只能在类外进行初始化

    int Student::m_total = 10;
    • 初始化可以不赋初值,不赋初值就是默认为0
    • 全局数据区的变量都有默认的初始值0,动态数据去(堆区、栈区)变量的默认值是不确定的,一般认为是垃圾值
  • 静态成员变量可以通过对象名访问,也可以通过类名访问,但是要遵循private、protected、public关键字访问权限限制。通过对象名访问的时候,虽然是不同的对象,但访问的是同一份内存

C++ static静态成员函数详解

  • 普通成员函数可以访问所有成员(包括成员变量和成员函数),静态成员函数只能访问静态成员
  • (不太理解)编译器在编译普通成员函数的时候会隐式增加形参this,并把当前对象的地址赋给this。所以普通成员函数只能在创建对象后通过对象来调用,因为需要当前对象的地址
  • (不太理解)静态成员函数可以通过类来直接调用,编译器不会为它增加形参this,它不需要当前对象的地址,所以不管有没有创建对象,都可以调用静态成员函数
  • 普通成员变量占用对象的内存,静态成员函数没有this指针,不知道指向哪个对象,无法访问对象的成员变量,所以静态成员函数只能访问静态成员变量
  • 普通成员函数必须通过对象才能调用,因为静态成员函数没有this指针,无法在函数体内部访问某个对象,所以不能调用普通成员函数,只能调用静态成员函数
  • 静态成员函数与普通成员函数的根本区别在于:普通成员函数有this指针,可以访问类中的任意成员;而静态成员函数没有this指针,只能访问静态成员(包括静态成员变量和静态成员函数)

例子

//该例通过静态成员函数来获得学生的总人数和总成绩
#include 
using namespace std;

class Student{
    public:
        Student(char *name,int age,float score);
        void show();
    public://声明静态成员函数
        static int getTotal();
        static float getPoints();
    private:
        static int m_total;//总人数
        static float m_points;//总成绩
    private:
        char *m_name;
        int m_age;
        float m_score;
};

int Student::m_total = 0;
float Student::m_points = 0.0;

Student::Student(char *name,int age,float score):m_name(name):m_age(age):m_score(score){
    m_total++;
    m_points += score;
}

void Student::show(){
    cout << m_name << "的年龄是" << "m_age" << ",的成绩是" << m_score << endl;
}

//定义静态成员函数
int Student::getTotal(){
    return m_total;
}

float Student::getPoints(){
    return m_points;
}

int main(){
    (new Student("小明",15,90.6)) -> show();
    (new Student("李磊",16,80.5)) -> show();
    (new Student("张华",16,99.0)) -> show();
    (new Student("王康",14,60.8)) -> show();

    int total = Student::getTotal();
    float points = Student::getPoints();
    cout << "当前共有" << total << "名学生,总成绩是" << points << ",平均分是" << points/total << endl;
    return 0;
}

result

  • 总人数m_total和总成绩m_points由各个对象累加得到,必须声明为static才能共享
  • getTotal()getPoint()分别用来获取总人数和总成绩,要将这两个函数也声明为static才能访问static成员变量
  • getTotal()getPoint()其实也可以声明为普通成员函数,但是它们都只对静态成员进行操作,加上static语义更加明确
  • 和静态成员变量类似,静态成员函数在声明的时候要加static,在定义的时候不加static。静态成员函数可以通过类来调用

C++ const成员变量和成员函数(常成员函数)

  • const成员函数可以使用所有成员变量,但不能改变成员变量的值。这种措施是为了保护数据而设置的,const成员函数也称为常成员函数

  • 我们通常将get函数设置为常成员函数,读取成员变量函数的名字通常以get开头,后跟成员变量的名字,通常称为get函数

  • 常成员函数需要在声明和定义的时候在函数头部的结尾加上const关键字

    class Student{
        public:
            Student(char *name,int age,float score);
            void show();
            //声明常成员函数
            char *getname() const;
            int getage() const;
            float getscore() const;
        private:
            char *m_name;
            int m_age;
            float m_score;
    }
    
    Student::Student(char *name,int age,float score);m_name(name),m_age(age),m_score(score){}
    void Student::show(){
        cout << m_name << "的年龄是" << m_age << ",成绩是" << m_score << endl;
    }
    //定义常成员函数
    char *Student::getname() const{
        return m_name;
    }
    
    int Student::getage() const{
        return m_age;
    }
    
    float Student::getstore() const{
        return m_score;
    }
    • getname()getage()getscore()三个函数的功能都很简单,仅仅是为了获取成员变量的值,不修改成员变量,所以加了const限制所以这样会比较保险,语义也会更明显
    • 在成员函数声明和定义的时候也要加上const关键字,char *getname() constchar *getname()是两个不同的函数原型,如果只在一个地方加const会导致声明和定义处的函数原型冲突
  • 最后,再来区分一下const的位置

    • 函数开头的const用来修饰函数的返回值表示返回值的是const类型,也就是不能被修改,比如const char *getname()
    • 函数头部的结尾加上const表示常成员函数,这种函数只能读取成员变量的值,不能修改成员变量的值,例如char *gername() const

C++ const对象(常对象)

在C++中,const也可修饰对象,称为常对象。对象一旦定义为常对象后,就只能调用类的const成员(包括const成员变量和const成员函数)

定义常对象的语法和定义常量的语法类似:

const class object(params);

class const object(params);

也可以定义const指针

const class *p = new class(params);

class const *p = new class(params);

class为类名,object为对象名,params为实参列表,p为指针名,两种方式定义出来的对象都是常对象

如果对const的用法不理解,看C语言const的用法详解

一旦将对象定义为常对象后,不管是哪种形式,该对象就只能访问被const修饰的成员了(包括const成员变量和const成员函数),因为非const成员可能会修改对象的数据(编译器也会这样去假设),C++不允许这样做

常对象使用举例

#include 
using namespace std;

class Student{
    public:
        Student(char *name,int age,float score);
    public:
        void show();
        char *getname() const;
        int getage() const;
        float getscore() const;
    private:
        char *m_name;
        int m_age;
        float m_score;
}

Student::Student(char *name,int age,float score):m_name(name),m_age(age),m_score(score){}
void Student::show(){
    cout << m_name << "的年龄是" << m_age <<",的成绩是" < show(); //error
    cout << pstu->getname() << "的年龄是" << pstu->getage() << ",成绩是" << pstu->getage() << endl;

    return 0;
}

本例中,stu、pstu分别是常对象以及常对象指针,它们都只能调用const成员函数

C++友元函数和友元类(C++ friend关键字)

  • C++中,类可以有public、protected、private三种属性的成员,通过对象可以访问public成员,只有本类中的函数可以访问本类中的private成员
  • 但是也有例外情况,比如友元(friend)。借助友元,可以使其他类中的成员函数以及全局范围内的函数访问当前类的private成员

友元函数

  • 只要在前面加friend关键字,就可以构成友元函数(友元函数可以不属于任何类,也可以是某个类的成员函数)
  • 友元函数可以访问当前类中所有的成员,包括public、protected、private属性的
将非成员函数声明为友元函数
#include 
using namespace std;

class Student{
    public:
        Student(char *name,int age,float score);
    public:
        friend void show(Student *pstu);//将show()声明为友元函数
    private:
        char *m_name;
        int m_age;
        float m_score;
};

Student::Student(char *name,int age,float score):m_name(name),m_age(age),m_score(score){}

//非成员函数
void show(Student *pstu){
    cout << pstu->m_name << "的年龄是" << pstu->m_age << ",成绩是" << pstu->m_score << endl;
}

int main(){
    Student stu("小明",15,90.6);
    show(&stu);//调用友元函数
    Student *pstu = new Student("李磊",16,80.5);
    show(pstu);//调用友元函数

    return 0;
}

//Result
小明的年龄是15,成绩是90.6
李磊的年龄是16,成绩是80.5
  • show是一个全局范围内的非成员函数,不属于任何类,它的作用就是输出学生的信息。m_name、m_age、m_score是Student类的private成员,原则上不能通过对象访问,但在show()函数中又必须使用这些private成员,所以将show()声明为Student类的友元函数

  • 注意:友元函数不同于类的成员函数,友元函数不能直接访问类的成员,必须要借助对象

    //这种写法是错误的
    void show(){
        cout << m_name << "的年龄是" << m_age << ",成绩是" << m_score << endl;
    }
  • 成员函数在调用的时候会隐式增加this指针,指向调用它的对象,从而使用该对象的成员

  • show()是非成员函数,没有this指针,编译器不知道要使用哪个对象的成员,要明确这一点,就必须通过参数传递对象(可以直接传递对象,也可以传递对象指针或者对象引用),并在访问成员的时候指明对象

将其他类的成员函数声明为友元函数
  • friend函数不仅可以是全局函数(非成员函数)还可以是另外一个类的成员函数
#include 
using namespace std;

class Address;//提前声明Address类

class Student{
    public:
        Student(char *name,int age,float score);
    public:
        void show(Address *addr);
    private:
        char *m_name;
        int m_age;
        float m_score;
};

//声明Address类
class Address{
    private:
        char *m_province;//省份
        char *m_city;//城市
        char *m_district;//区
    public:
        Address(char *province,char *city,char *district);
        //将Student类中的成员函数show()声明为友元函数
        friend void Student::show(Address *addr);
};

//实现Student类
Student::Student(char *name,int age,float score):m_name(name),m_age(age),m_socre(score){}
void Student::show(Address *addr){
    cout << m_name << "的年龄是" << m_age << ",成绩是" << m_score << endl;
    cout << "家庭住址:" << addr->m_province << "省" << addr->m_city << "市" << addr->district << "区" << endl;
}

//实现Address类
Address::Address(char *province,char *city,char *district){
    m_province = province;
    m_city = city;
    m_district = district;
}

int main(){
    Student stu("小明",16,95.5f);
    Address addr("福建","龙岩","新罗");
    stu.show(&addr);

    Student *pstu = new Student("李磊",16,80.5);
    Address *paddr = new Address("北京","北京","海淀");
    pstu->show(paddr);

    return 0;
}

需要注意的几个点

  • 程序提前对Address类进行了声明,因为在Address类定义之前,在Student类中用到了它,如果不提前声明编译器就会报错,提示'Address' has not been declared。类的提前声明和函数的提前声明是一样的道理

  • 程序将Student类的声明和实现分开,将Address类的声明放在中间,因为编译器从上到下编译代码,show()函数体中用到Address的成员province、city、district,如果不提前知道Address 的具体声明内容,就不能确定Address是否拥有该成员(类的声明中有指明类有哪些成员)

  • 一般情况下,类要正式声明后才能使用,但在某些情况下,只要做好提前声明也可以先使用

  • 但是类要正式声明后才能去创建对象,否则会报错

    因为创建对象的时候要为对象分配内存,在正式声明类之前,编译器无法确定要分配多大内存给类。编译器只有见到正式的声明后(主要是成员变量),才能确定要为对象预留多大的内存

    对一个类做提前声明,就可以用该类的名字去定义指向该类型对象的指针变量(因为指针和引用变量的大小是固定的)

  • 一个函数可以被多个类声明为友元函数,这样就可以访问多个类中的private成员

友元类

  • 将一个类声明为另外一个类的“朋友”,就是友元类,友元类中所有成员函数都是另外一个类的友元函数

    例如将类B声明为类A的友元类,那么类B中所有的成员函数都是类A的友元函数,可以访问类A中所有的成员,包括public、protected、private属性的

#include 
using namespace std;

class Address;//提前声明Address类

//声明Student类
class Student{
    public:
        Student(char *name,int age,float score);
    public:
        void show(Address *addr);
    private:
        char *m_name;
        int m_age;
        float m_score;
};

//声明Address类
class Address{
    public:
        Address(cahr *ptovince,char *city,char *district);
    public:
        //将Student类声明为Address类的友元类
        friend class Student;
    private:
        char *m_province;//省份
        char *m_city;//城市
        char *m_district;//区(市区)
};

//实现Student类
Student::Student(char *name,int age,float score):m_name(name),m_age(age),m_score(score){}
void Student::show(Address *addr){
    cout << m_name << "的年龄是" << m_age << ",成绩是" << m_score << endl;
    cout << "家庭住址:" << addr->m_province << "省" << addr->m_city << "市" << addr->m_district << "区" <show(paddr);

    return 0;
}

友元声明语句

friend class Student;

有的编译器可以不写class关键字,不过为了增强兼容性写上会比较好

  • 友元的关系是单向的不是双向的
  • 友元的关系不能传递
  • 一般也不建议把整个类声明为友元类,只将某些成员函数声明为友元函数会更安全一些

类也是一种作用域

  • 每种类都会定义自己的作用域,在类的作用域之外,普通的成员只能通过对象(可以是对象的本身,也可以是对象指针或对象引用)来访问,静态成员可以通过对象访问也可以通过类访问,而typedef定义的类型只能通过类来访问
//用不同的方式访问不同的成员
#include 
using namespace std;

class A{
    public:
        typedef int INT;
        static void show();
        void work();
};
void A::show(){cout << "show()" << endl;}
void A::work(){cout << "work()" << endl;}

int main(){
    A a;
    a.work();//通过对象访问普通成员
    a.show();//通过对象访问静态成员
    A::show();//通过类访问静态成员
    A::INT n = 10;//通过类访问typedef定义的类型

    return 0;
}

定义在类外部的成员

  • 一个类就是一个作用域,所以在类的外部定义成员函数的时候必须同时提供类名和函数名。在类的外部,类的内部成员名字是不可见的
  • 如果遇到类名,参数列表和函数体就在类的作用域之内。我们就可以直接使用类和其他成员无需再次授权
#include 
using namespace std;

class A{
    public:
        typedef char* PCHAR;
    public:
        void show(PCHAR str);
    private:
        int n;
};

void A::show(PCHAR str){
    cout << str << endl;
    n = 10;
}

int main(){
    A obj;
    obj.show("test");

    return 0;
}
  • 因为已经在类A中定义了PCHAR,也还是位于A类的作用域中,所以不再使用A::PCHAR这样的形式。同理n也是在A类的作用域中
  • 函数的返回值类型出现在函数名的前面,成员函数在类的外部定义的时候,返回中类型中使用的名字都位于类的作用域之外,此时必须要指明这个名字是哪个类的成员,比如:修改上面的show()函数,让它的返回值类型为PCHAR
//这是错的
PCHAR A::show(PCHAR str){
    cout << str << endl;
    n = 10;
    return str;
}
  • 这种写法是错的,因为返回值的类型PCHAR在类名的前面,意思就是它位于A的作用域之外,正确的写法如下
A::PCHAR A::show(PCHAR str){
    cout << str << endl;
    n = 10;
    return str;
}

C++ class和struct的区别

  • C的struct只能包含成员变量,不能包含成员函数;C++的struct类似于class,能包含成员变量也能包含成员函数

C++中struct和class的小区别

反面例子

//用struct定义类的反面教材
#include 
using namespace std;

struct Student{
    Student(char *name,int age,float score);
    void show();

    char *m_name;
    int m_age;
    float m_score;
};

Student::Student(char *name,int age,float score):m_name(name),m_age(age),m_score(score){}
void Student::show(){
    cout << m_name << "的年龄是" << m_age <<",成绩是" << m_score << endl;
}

int main(){
    Student stu("小明",15,92.5f);
    stu.show();
    Student *pstu = new Student("李华",16,96);
    pstu->show();

    return 0;
}
  • 这段代码可以通过编译,说明struct默认的成员都是public属性的,否则不能通过对象访问成员函数。如果将struct关键字替换为class,就会编译报错

C++ string详解,C++字符串详解

  • C++可以使用C语言的字符串,也可以使用内置的string类

  • string类用来处理字符串很方便,可代替C语言中的字符数组或字符串指针

  • string是C++中常用的一个类,非常重要

  • 使用string需要包含头文件string

    #include 
    #include 
    using namespace std;
    
    int main(){
        string s1;
        string s2 = "c plus plus";
        string s3 = s2;
        string s4 (5,'s');
    
        return 0;
    }
    • 变量s1只是定义,但是没有初始化,编译器会把默认值赋给s1,默认值是””也就是空字符串
    • 变量s2在定义的同时被初始化为”c plus plus”,与C语言不同,string的结尾没有结束标志”\0”
    • 变量s3在定义的时候直接使用s2初始化,s3的内容也是”c plus plus”
    • 变量s4被初始化为由5个”s”字符组成的字符串,也就是”sssss”
  • 我们需要知道字符串长度的时候,可以调用string类提供的length()函数

    string s = "test test";
    int len = s.length();
    cout << len << endl;
    //因为C++ string的结尾没有'\0’字符,所以length()返回的是字符串真实的长度 不是长度+1

转换为C风格的字符串

  • string类为我们提供了一个转换函数c_str(),该函数能将string字符串转换为C风格的字符串,并返回该字符串的const指针(const char*)

    string path = "D:\\demo.txt";
    FILE *fp = fopen(path.c_str(),"rt");
    //用C语言中的fopen()函数打开文件,就必须将string字符串转换为C风格的字符串

string字符串的输入输出

  • string类重载了输入输出运算符,可以像对待普通变量那样去对待string变量,用>>进行输入,<<进行输出

    #include 
    #include 
    
    using namespace std;
    
    int main(){
        string s;
        cin >> s;//输入字符串
        cout << s << endl;//输出字符串
        return 0;
    }
  • 运算符>>默认忽略空格,遇到空格就会以为输入结束

访问字符串中的字符

  • string字符串也可以像C风格的字符串一样按照下标来访问其中的每一个字符,string字符串的起始下标仍然是从0开始

    #include 
    #include 
    using namespace std;
    
    int main(){
        string s = "1234567890";
        for(int i=0,len=s.length();i

字符串的拼接

  • 有了string类后,我们可以使用++=来直接拼接字符串,就不需要strcat()strcpy()malloc()这些函数来拼接字符串了,也就不用担心空间不够会溢出了

  • +来拼接字符串的时候,运算符两边都可以是string字符串,也可以是string字符串+C风格的字符串string字符+字符数组string字符串+一个单独的字符

    #include 
    using namespace std;
    
    int main(){
        string s1 = "first";
        string s2 = "second";
        char *s3 = "third";//C风格字符串?
        char s4[] = "fourth";//字符数组
        char ch = '@';//字符
    
        string s5 = s1 + s2;
        string s6 = s1 + s3;
        string s7 = s1 + s4;
        string s8 = s1 + ch;
    
        cout << s5 << endl << s6 << endl << s7 << endl << s8 << endl;
    
        return 0;
    }

string字符串的增删改查

  • C++提供的string类包含了若干实用的成员函数,大大方便了字符串的增加、删除、更改、查询等
插入字符串
  • insert()函数可以在string字符串中指定的位置插入另一个字符串,原型为:

    string& insert(size_t pos,const string& str);
  • pos表示要插入的位置,也就是下标;str表示要插入的字符串,可以说string字符串或者C风格的字符串

    #include 
    #include 
    using namespace std;
    
    int main(){
        string s1,s2,s3;
        s1 = s2 = "1234567890";
        s3 = "aaa";
        s1.insert(5,s3);
        cout << s1 << endl;
        s2.insert(5,"bbb");
        cout << s2 << endl;
        return 0;
    }
  • insert()函数的第一个参数有越界的可能,越界的话会产生运行的异常,可以在C++异常(Exception)_C语言中文网 (biancheng.net)中查看

  • 更多insert()函数的原型和用法string::insert - C++ Reference (cplusplus.com)

删除字符串
  • erase()函数可以删除string中的一个子字符串,原型为:

    string& erase(size_t pos = 0,size_t len = npos);
  • pos表示要删除子字符串的起始下标,len表示要删除子字符串的长度

  • 如果不指明len的话,就直接删除从pos到字符串结束处的所有字符(此时len = str.length-pos)

    #include 
    #include 
    using namespace std;
    
    int main(){
        string s1,s2,s3;
        s1 = s2 = s3 = "1234567890";
        s2.erase(5);
        s3.erase(5,3);
        cout << s1 << endl;
        cout << s2 << endl;
        cout << s3 << endl;
    
        return 0;
    }
    //Result
    1234567890
    12345
    1234590
  • 在pos参数没有越界的情况下,erase()函数会从以下两个值中取出一个最小的值作为待删除子字符串长度

    • len的值
    • 字符串长度减去pos的值

    简单来说,待删除字符串最多智能删除到字符串结尾

提取字符串
  • substr()函数用于从string字符串中提取子字符串,原型为:

    string substr(size_t pos = 0,size_t len = npos)const;
  • pos表示要提取字符串的起始下标,len为要提取子字符串的长度

    #include 
    #include 
    using namespace std;
    
    int main(){
        string s1 = "first second thrid";
        string s2;
        s2 = s1.substr(6,6);
        cout << s1 << endl;
        cout << s2 << endl;
        return 0;
    }
    
    //Result
    first second thrid
    second
  • 系统对substr()参数的处理与erase()类似

  • 如果pos越界,会抛出异常

  • 如果len越界,会提取从pos到字符串结尾处的所有字符(就是最大就到结尾了

字符串查找

有3个与字符串查找相关的函数

find函数
  • find函数用于在string字符串中查找子字符串出现的位置,其中的两种原型为:

    size_t find(const string&str,size_t pos = 0)const;
    size_t find(const char* s,size_t pos = 0)const;
  • 第一个参数为待查找的子字符串,可以是string字符串,也可以是C风格字符串

  • 第二个参数为开始查找的位置(下标),如果不指明,就从第0个字符开始查找

    #include 
    #include 
    using namespace std;
    
    int main(){
        string s1 = "first second third";
        string s2 = "second";
        int index = s1.find(s2,5);
        if(index < s1.length())
            cout << "Found at index : " << index << endl;
        else
            cout << "Not Found" << endl;
        return 0;
    }
    //Result
    Found at index  : 6
  • find()函数最终返回的子字符串第一次出现在字符串中的起始下标,本例是在下标6处找到了s2字符,如果没有找到子字符串,就会返回一个无穷大值。

rfind函数
  • rfind()与find()类似,都是在字符串中查找子字符

  • 区别是find()函数从第二个参数开始往后查找,rfind()函数最多查找到第二个参数处

  • 如果到了第二个参数指定的下标还没有找到子字符串,就会返回一个无穷大值

    #include 
    #include 
    using namespace std;
    
    int main(){
        string s1 = "first second third";
        string s2 = "second";
        int index = s1.rfind(s2,6);
        if(index < s1.length())
            cout << "Found at index : " << index << endl;
        else
            cout << "Not found" << endl;
    
        return 0;
    }
    
    //Result
    Found at index : 6
find_first_of函数
  • find_first_of()函数用于查找子字符串和字符串共同具有的字符在字符串中首次出现的位置

    #include 
    #include 
    using namespace std;
    
    int main(){
        string s1 = "first second third";
        string s2 = "asecond";
        int index = s1.find_first_of(s2);
        if(index < s1.length())
            cout << "Found at index : " << index << endl;
        else
            cout << "Not found" << endl;
        return 0;
    }
    
    //Result
    Found at index : 3
  • s1和s2共同的字符是“s”,在s1中首次出现的下标是3

C++ string的内部是什么样的

C语言中表示string的方式

  • 用字符数组来容纳字符串,char str[10] = “abc",可读写
  • 字符串常量char str = “abc”,只读不能写*(why?)

C语言的字符串都是以\0作为结束标志的

C++中表示string的方式

C++ string隐藏了所包含的字符序列的物理表示(好绕口),程序员不需要关心数组的维数和\0的问题了(C++字符串不用以\0结尾了

  • C++ string在内部封装了与内存、容量相关的信息。C++ string对象可以知道自己在内存中的开始位置包含的字符序列以及字符序列的长度;当内存空间不足的时候,string还会自动调整,让内存空间增长到足以容纳下所有字符序列的大小
C语言三种常见破坏性错误

C++ string的这种做法极大减少了C语言编程中三种最常见且最具破坏性的错误:

  • 数组越界
  • 通过未被初始化或者被赋以错误值的指针来访问数组的元素
  • 释放了数组所占的内存,但是仍然保留了”悬空“指针(也就是野指针,指针仍然指向以及回收的内存地址)

但是C++的标准没有定义string类的内存布局,所以各个编译器的实现方式可能都不一样,但是必须要保证string行为的一致性。

C++标准没有定义在哪种确切的情况下应该为string对象分配内存空间来存储字符序列,string内存分配规则必须明确规定:允许但不要求以引用计数(reference counting)的方式来实现,但无论是否采用引用计数,语义都必须一致

C++的此种做法与C语言不同,C语言中每个字符型数组都占据各自的物理存储区。在C++中,独立的几个string对象可以占据也可以不占据各自特定的物理存储区。但是如果采用引用技术就可以避免保存了同一个数据的拷贝副本,各个独立的对象(在处理上)就必须看起来并表现得就像独占地拥有各自的存储区

#include 
#include 
using namespace std;

int main(){
    string s1("12345");
    string s2 = s1;
    cout << (s1 == s2) << endl;
    s1[0] = '6';
    cout << "s1 = " << s1 << endl;//62345
    cout << "s2 = " << s2 << endl;//12345
    cout << (s1 == s2) << endl;

    return 0;
}

//gcc Result
1
s1 = 62345
s2 = 12345
0

只有当字符串被修改的时候才会创建各自的拷贝,这种实现方法称为写时复制(copy-on-write)策略,当字符串只是作为值参数(value parameter)或在其他只读情形下使用,这种方法能够节省时间和空间

不论一个库的实现是不是采用引用计数,它对string类的使用者来说都应该是透明的,但是在多线程程序中,几乎不可能安全地使用引用计数来实现

C++类和对象的总结

  • 类的成员有成员变量成员函数两种
  • 成员函数之间可以互相调用,成员函数内部可以访问成员变量
  • 私有成员只能在类的成员函数内部访问,默认情况下,class类的成员是私有的,struct类的成员是公有的
  • 可以用”对象名.成员名”、“引用名.成员名“、”对象指针->成员名“的方法来访问对象的成员变量或调用成员函数。成员函数被调用的时候,可以用上面三种方法指定函数是作用在哪个对象上
  • 对象所占用的存储空间大小等于各成员变量所占用的存储空间大小之和(不考虑成员变量对齐问题)
  • 定义类的时候,如果一个构造函数都不写,编译器会自动生成默认(无参)构造函数和复制构造函数。如果编写了构造函数,编译器不自动生成默认构造函数,一个类不一定会有默认构造函数,但一定会有复制构造函数
  • 任何生成对象的语句都必须说明对象是用哪个构造函数初始化的
  • 定义对象数组,也要对数组中的每个元素如何初始化进行说明。如果不说明编译器就会认为对象是用默认构造函数或参数全部可以省略的构造函数初始化的。这种情况下,如果类没有默认构造函数或者参数可以全部省略的构造函数就会编译出错
  • 对象在消亡的时候会调用析构函数
  • 每个对象有各自独特的一份普通成员变量,但是static静态成员变量只有一份会被所有对象共享(不知道是不是要同一个类里面,有点忘了)静态成员函数不具体作用于某个对象,即使对象不存在,也可以访问类的静态成员。静态成员函数内部不能访问非静态成员变量,也不能调用非静态成员函数
  • 常量对象上面不能指向非常量成员函数,只能执行常量成员函数
  • 包含成员对象的类叫做封闭类。任何能够生成封闭类对象的语句都要说明对象中包含的成员对象是如何初始化的。如果不说明,则编译器会认为成员对象是用默认构造函数或参数全部可以省略的构造函数初始化的
  • 在封闭类的构造函数初始化列表可以说明成员对象如何初始化,封闭类对象生成的时候,先执行成员对象的构造函数,再执行自身的构造函数;封闭类对象消亡的时候,先执行自身的构造函数,再执行成员对象的构造函数
  • const成员和引用成员必须在构造函数的初始化列表中初始化,此后值不可修改
  • 友元分为友元函数友元类,友元关系不能传递
  • 成员函数中出现的this指针,就是指向成员函数所作用对象的指针。因此,静态成员函数内部不能出现this指针。成员函数实际上的参数个数比表面看到的多一个,多出来的参数就是this指针

   转载规则


《C++学习笔记》 InImpasse 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录