C++(week10):C++基础: 第二章 类与对象

文章目录

二、类与对象

1.面向对象的思想

面向过程、面向对象

面向对象,先有对象,抽象为类。

抽象、封装、继承、多态

2.类的定义

(1)类与对象

由对象抽象出类,由类实例化出对象

C++中用关键字class来定义一个类,其基本形式如下:类的定义和声明

cpp 复制代码
class MyClass{//类的定义
    //......
    void myFunc(){}  //成员函数
    int _a;          //数据成员
};//一定要有分号


//类也可以先声明,后完成定义
class MyClass2;//类的声明

class MyClass2{//类的定义
    //......
};//分号不能省略

(2)类的成员

1.类的成员:数据成员、成员函数

2.类名遵循大驼峰原则,如class ComputerTest

成员函数的名称遵循小驼峰原则,如setBrand(const char * brand)

①数据成员

加下划线 区分,是类的数据成员,还是外部传入的参数。或者用this指针

cpp 复制代码
class Computer
{
	//数据成员的名称前面加上下划线,与传入参数进行区分
	char _brand[20];
	double _price;
}
②成员函数

1.类的非静态成员函数,第一个参数是隐式传递的this指针,指向调用该成员函数的对象。

2.this 指针:在非静态成员函数中,编译器会隐式地传递一个指向调用对象的指针,称为 this 指针。通过 this 指针,成员函数可以访问和修改调用对象的成员变量和成员函数。

3.一个空类也默认有6个成员函数:

①无参构造函数 ②析构函数 ③拷贝构造函数 ④赋值运算符函数 ⑤operator new ⑥operator delete

(3)访问修饰符:三种访问权限

public :公有的访问权限,在类外可以通过对象直接访问公有成员。

public的成员函数也可以称为接口,就是该类提供给外界使用的路径,在类外可以直接通过对象进行访问。

protected:保护的访问权限,在本类中和派生类中可以访问,在类外不能通过对象直接访问(后面学)

private :私有的访问权限,在本类之外不能访问,比较敏感的数据设为private,类定义中可以访问。

若未声明访问权限,默认为私有。

类会一次性将所有内容读入,因此即使数据成员在成员函数之后,也不会报错。

Google编程规范:成员函数共有,数据成员私有。公有成员放前面 (先看到接口),私有成员放后面。

(4)class 与 struct 的对比

1.C语言中struct:

①只能封装变量,不能封装函数

②没有访问权限限制,无法阻止访问

c 复制代码
struct Student{
    int number;
    char name[25];
    int score;
};

void test0(){
    struct Student s1;
    struct Student s2 = {10,"Jack",98};
}

2.C++中的struct:

C++中的struct对C中的struct做了拓展,基本等同于class,默认访问权限是public。而C++中的class的默认访问权限是private

(5)类的成员函数的声明与定义

类中内容过多时,可以考虑成员函数的声明与实现分离:

在类中只留下声明,在类的外部进行实现。需要加类名 + 作用域限定符

多文件联合编译时可能出现冲重定义错误,有三种解决方法:

1.声明与实现分别放在头文件与实现文件里

2.都放在头文件中,实现加上inline (不加inline就联合编译,会报错multiple definition)

3.将成员函数放到类内部进行定义(说明类内部定义的成员函数就是inline函数------默认效果)

cpp 复制代码
#include <string.h>
#include <iostream> 
using std::cout;
using std::endl;

//自定义类
class Student{
public:
    Student():_id(1),_height(180),_name("Ed"),_score(100)
    {
        cout << "Student()" << endl;
    }

    Student(int id, double height, const char * name, double score)
    :_id(id),_height(height), _score(score)
    {
        cout << "Student(int id, double height, const char * name, double score)"  << endl;
        strcpy(_name, name);
        _name[sizeof(_name) - 1] = '\0';  //确保C风格字符串以'\0'结尾
    }

    int getId(){
        return _id;
    }

    double getHeight(){
        return _height;
    }

    char * getName(){
        return _name;
    }

    double getScore(){
        return _score;
    }

private:
    int _id;
    double _height;
    char _name[20];
    double _score;
};

int main()
{
    Student stu;
    
    int id = stu.getId();
    cout << "id = " << id << endl;

    double height = stu.getHeight();
    cout << "height = " << height << endl;

    char * name = stu.getName();
    cout << "name = " << name << endl;

    double score = stu.getScore();
    cout << "score = " << score << endl;
    
    cout << "----------------------" << endl;

    Student stu2(2, 182.0, "Edward", 140.5);
    int id2 = stu2.getId();
    cout << "id2 = " << id2 << endl;

    double height2 = stu2.getHeight();
    cout << "height2 = " << height2 << endl;

    char * name2 = stu2.getName();
    cout << "name2 = " << name2 << endl;

    double score2 = stu2.getScore();
    cout << "score2 = " << score2 << endl;
    
    return 0;
}

3.对象的创建

(1)构造函数

在之前的 Computer 类中,通过自定义的公共成员函数 setBrand 和 setPrice 实现了对数据成员的初始化(严格意义上是赋值)。

实际上,C++ 为类提供了一种特殊的成员函数------构造函数,来完成真正的初始化。

1.构造函数的作用:初始化对象的数据成员

2.构造函数的形式:

①没有返回类型

②函数名与类名相同,再加上函数参数列表、函数体

3.构造函数的调用时机:

构造函数在创建对象 时(由编译器加上一段代码)自动调用,用以完成对象成员变量等的初始化及其他操作 (如为指针成员动态申请内存等)

4.构造函数的特点 (构造函数的使用规则):

①当类中没有显式定义构造函数时 ,编译器会自动生成一个默认 (无参) 构造函数 ,但并不会初始化数据成员;

②一旦当类中显式提供了构造函数时 ,编译器就不会再自动生成默认的构造函数;

③编译器自动生成的默认构造函数是无参的,构造函数也可以接收参数 (显示定义有参构造函数),在对象创建时提供更大的自由度;

④如果还希望通过默认构造函数创建对象, 则必须要手动提供一个默认构造函数;

⑤构造函数还可以重载,一个类可以定义多个构造函数,即创建对象时可以用多种方式对数据成员进行初始化。

(2)初始化列表

1.初始化列表 对 数据成员进行了严格的初始化

2.数据成员的初始化顺序 与 声明的顺序 一致 。与数据成员在初始化列表中的顺序无关。

3.初始化列表只有构造函数和拷贝构造函数才可以使用。

cpp 复制代码
Point():_ix(0),_iy(0){
	cout << "Point()" << endl;
}

Point(int x, int y):_ix(x),_iy(y){
	cout << "Point(int, int)" << endl;
}
cpp 复制代码
#include <iostream> 
using std::cout;
using std::endl;

class Point{
public:
    /* Point(){ */
    /*     cout << "Point()" << endl; */
    /*     _ix = 0; */
    /*     _iy = 0; */
    /* } */
    
    Point(): _ix(0), _iy(0)
    {
        cout << "Point()" << endl;
    }
    
    /* Point(int x){ */
    /*     cout << "Point(int)" << endl; */
    /*     _ix = x; */
    /*     _iy = x + 10; */
    /* } */

    Point(int x): _ix(x), _iy(x+10)
    {
        cout << "Point(int)" << endl;
    }

    /* Point(int x, int y){ */
    /*     cout << "Point(int,int)"  << endl; */
    /*     _ix = x; */
    /*     _iy = y; */
    /* } */

    Point(int x, int y): _ix(x), _iy(y)
    {
        cout << "Point(int,int)"  << endl;
    }

    void setX(int x){
        _ix = x;
    }

    void setY(int y){
        _iy = y;
    }

    void print(){
        cout << "(" << _ix << "," << _iy << ")" << endl;
    }

private:
    int _ix;
    int _iy;
};

void test0(){
    //调用有参构造
    Point pt2(1, 2);
    pt2.print();

    //调用无参构造的方式
    Point pt;
    pt.print();

    Point pt3(3);
    pt3.print();
}

int main()
{
    test0();   
    return 0;
}

(3)对象所占空间大小

1.对象所占空间大小 = 数据成员的大小 (要考虑内存对齐),成员函数在程序代码段,不占大小。

2.Q:为什么要进行内存对齐?

A:为了提升性能,不读两次内存,64位系统默认以8个字节的块大小进行读取

①除数组外,其他类型的数据成员中,以较大的数据成员所占空间的倍数去对齐。

②内存对齐还与数据成员的声明顺序有关。


3.类中的数据成员较多时,可以设计一下数据成员的声明顺序,以节省空间。

因为都按照除了数组外的最大的数据成员的类型进行内存对齐,那么短的数据类型就可以拼一行。

4.#pragma pack(n)

在C语言的涉及的结构体代码中,我们可能会看到#pragma pack的一些设置,#pragma pack(n)即设置编译器按照n个字节对齐,n可以取值1,2,4,8,16.在C++中也可以使用这个设置,最终的对齐效果将按照 #pragma pack 指定的数值和类中最大的数据成员长度 中,比较小的那个的倍数进行对齐。

5.空类的大小为1个字节。(编译器的一种占位机制)

(4)指针数据成员

堆空间

4.对象的销毁

(1)析构函数

1.析构函数:对象在销毁时,一定会调用析构函数

2.析构函数的作用:清理对象的数据成员申请的资源(堆空间)------ 析构函数并不负责清理数据成员(系统自动完成)

3.析构函数的形式: 【特殊的成员函数】
~类名(){}

①没有返回值 (即使是void也没有)

②没有参数 (析构函数是无参的,因此析构函数只有一个,不能发生函数重载)

4.默认情况下 ,系统也会自动提供一个析构函数

5.析构函数的调用时机:当对象被销毁时 ,操作系统会自动调用析构函数

6.析构函数用来销毁指针数据成员申请的堆空间。

在栈上的类的数据成员的回收由系统自动回收。

7.先回收数据成员申请的堆空间,再回收数据成员。

(2)自定义析构函数

1.之前的例子中,我们没有显式定义出析构函数,但是没有问题,系统会自动提供一个默认的析构。
析构函数作为一个清理数据成员申请的堆空间的接口存在

当数据成员中有指针时,创建一个对象,会申请堆空间,销毁对象时默认析构不够用了(造成内存泄漏),此时就需要我们自定义析构函数。在析构函数中定义堆空间上内存回收的机制,就不会发生内存泄漏。

2.安全回收 (回收后置为nullptr)

cpp 复制代码
//标准的析构函数
~Computer()
{
	if(_brand){
    	delete [] _brand;
        _brand = nullptr//设为空指针,安全回收
       }
	cout << "~Computer()" << endl;
}

3.对象被销毁,一定会调用析构函数。

但调用析构函数,只会回收数据成员申请的堆空间,并不会销毁对象

(3)析构函数的调用时机 (重点)

1.对于全局对象整个程序结束时,自动调用全局对象的析构函数。

2.对于局部对象 ,在程序离开局部对象的作用域时调用对象的析构函数。

3.对于静态对象 ,在整个程序结束时调用析构函数。

4.对于 堆对象在使用 delete 删除该对象时,调用析构函数。


5.本类型对象的复制

(1)拷贝构造函数

CopyConstructor

①拷贝构造函数的定义
cpp 复制代码
类名(const 类名 & ) 
cpp 复制代码
Point(const Point & rhs)
: _ix(rhs._ix)
, _iy(rhs._iy)
{
	cout << "Point(const Point &)" << endl;
}

1.该函数是一个构造函数 ------ 拷贝构造函数也是构造函数

2.该函数用一个已经存在的同类型的对象,来初始化新对象,即对对象本身进行复制

没有显式定义拷贝构造函数,这条复制语句依然可以通过,说明编译器自动提供了默认的拷贝构造函数。其形式是:

cpp 复制代码
Point(const Point & rhs)
: _ix(rhs._ix)
, _iy(rhs._iy)
{}

3.默认的拷贝构造是浅拷贝。如拷贝 char *,可能造成double free。

需要自定义拷贝构造函数,new一片空间再strcpy,即深拷贝。

<1>深拷贝与浅拷贝

深拷贝,开辟新空间,再复制内容。

浅拷贝,仅仅改变指针的指向,不开辟新空间。通常与引用计数联用。否则容易double free。

1.浅拷贝:仅复制对象的引用,新旧对象共享同一份数据。不创建新对象,仅仅复制引用。

比如,char * 的浅拷贝,让两个指针指向同一个内容。可能造成double free

2.深拷贝:复制对象及其引用的所有数据,两个对象互不影响。创建一个新对象,复制新对象

②拷贝构造函数的三种调用时机 (重点)

1.用已经存在的对象创建并初始化同类型的新对象

cpp 复制代码
Point pt2 = pt1;
cpp 复制代码
MyClass obj1;		 // 调用无参构造函数
MyClass obj2 = obj1; // 类名开头,是用已经存在的对象创建新对象。是调用拷贝构造函数
MyClass obj2(obj1);  // 用括号和用等号等价

2.对象作为参数进行传递时,实参初始化形参

且参数是从右往左进行赋值。(具体的参数赋值顺序和编译器有关,但gcc和vs都是从右往左)

当函数参数的实参和形参的类型都是对象,利用实参初始化形参,相当于是值传递,会发生复制,会调用拷贝构造。------ 为了避免这次多余的复制,可以将形参改为引用,避免了复制,也就避免了拷贝构造函数的调用。

cpp 复制代码
void func(Point pt){
	
}

func(pt);
cpp 复制代码
void func(MyClass obj) {//值传递,实参初始化形参,调用拷贝构造函数
    //...
}

MyClass obj;
func(obj); // 调用拷贝构造函数

3.函数返回时,函数的返回值是对象

当函数的返回值是对象,执行return语句时,会发生复制 (编译器有优化)。

------为了避免这次多余的拷贝,可以使用引用作为返回值,但一定要确保引用绑定的对象本体的生命周期比函数更长。(返回值的生命周期大于函数的生命周期)

cpp 复制代码
return pt1;
cpp 复制代码
MyClass func() {
    MyClass obj;
    return obj; // 调用拷贝构造函数
}

MyClass obj = func(); // 可能会调用拷贝构造函数(实际中可能会有RVO优化)

第三种情况直接编译并不会显示拷贝构造函数的调用,但是底层实际是调用了的,加上去优化参数进行编译可以看到效果

bash 复制代码
g++ CopyComputer.cc -fno-elide-constructors
③拷贝构造函数的参数形式探究

1.引用的作用:

会触发拷贝构造函数的第二种调用时机,const Point rhs = pt,会无限递归调用下去,直至栈溢出导致程序崩溃。
Point(const Point & rhs)

2.const的作用:

(1)加const的引用 是常引用,可以避免拷贝构造函数中修改数据成员的值。【防修改】

本来是复制的,但是不安好心的拷贝构造函数可以在其内部修改数据成员的值。导致复制前后数据成员的值不一样。

(2)非const引用,只能绑定左值,不能绑定右值。【可以绑右值】
const引用(常引用)可以绑定右值,即临时对象

左值与右值

1.左值 (L-value,locator value):能够取地址,叫做左值。左值可以出现在赋值运算符的左边。

2.右值 (R-value,read value):不能取地址的,叫做右值。不能出现在赋值运算符的左边。如 临时变量(匿名变量)、临时对象(匿名对象)、字面值常量。

3.①匿名对象的生命周期只在当前行。

②非const引用,只能绑定左值,不能绑定右值

④对象作为函数的参数,加&和const的作用

可以是distance(Point lhs, Point rhs); 但最好是 distance(const Point & lhs, const Point & rhs);

(1)用 & 可以避免值传递时的拷贝

(2)const:①避免在函数内修改传入的对象的值 ②可以接右值

(2)赋值运算符函数

①概念、语法、四部曲构成

赋值运算符函数(Assignment Operator Function)是在C++中重载的一种特殊函数,用于自定义类对象的赋值行为。在C++中,赋值运算符通常用来将一个对象的状态复制给另一个对象。重载了=运算符

默认情况 下,C++编译器提供了一个简单的成员赋值运算符(`='),它执行浅拷贝 (浅层复制),即简单地将一个对象的成员变量的值复制到另一个对象中。

②如果需要更复杂的行为,例如深拷贝 (深层复制,包括动态分配的内存)、资源管理或其他特定于类的操作,就需要定义自己的赋值运算符函数。这可以通过重载赋值运算符 = 来实现,其语法如下:

cpp 复制代码
类名 &类名 :: operator= (const 类名 & 对象名) {  }
cpp 复制代码
//没有指针成员的情况
Computer & operator= (const Computer & rhs){
	this->_brand = rhs._brand;
	this->_price = rhs._price;
	return *this;
}
cpp 复制代码
//类中有指针成员动态申请堆空间的情况
Computer & operator= (const Cpmputer & rhs){
	cout << "赋值运算符函数" << endl;
	if(this != &rhs){	//1.判断是否是自复制
		delete [] _brand; //2.回收左操作数的数据成员原本管理的堆空间
		_brand = new char[strlen(rhs._brand) + 1]();  //3.深拷贝
		strcpy(_brand, rhs._brand);
		_price = rhs._price;  //以及其他的数据成员完成简单赋值
	}
	return *this;  //4.返回本对象
}

规范的赋值运算符函数的构成:

(1)判断是否为自复制

(2)回收左操作数的数据成员申请的堆空间

(3)深拷贝(以及其他的数据成员的复制)

(4)返回*this(本对象)

②赋值运算符函数的调用时机

进行赋值操作(前面没有类型名,有类型名就是创建对象,调用的是拷贝构造函数)

cpp 复制代码
Point pt(1), pt2(2);
pt = pt2; //没有类型,用已经存在的对象赋值给另一个已存在的对象,调用赋值运算符函数
pt.operator=(pt2);  //本质

赋值操作表达式的返回值是左操作数:

③this指针

1.this指针指向本对象 (当前对象)。this指针记录的是本次调用成员函数的对象的地址

2.this指针的生命周期:

①this 指针的生命周期开始于对象调用成员函数执行开始 。当一个非静态成员函数被调用时,this 指针被自动设置为指向调用该函数的对象实例。在成员函数执行期间,this 指针一直有效。它可以被用来访问调用对象的成员变量和成员函数。

②this指针的生命周期结束于对象调用成员函数的执行结束。当成员函数返回时,this指针的作用范围就结束了。

3.this指针本质是常量指针Type * const pointer,不可修改指向。

4.this指针存在哪儿?
this指针不能取地址,因为不在内存中,在寄存器中。

编译器在生成程序时加入了获取对象首地址的相关代码,将获取的首地址存放在了寄存器中,这就是this指针。

5.编译器会自动给成员函数加上this指针,作为成员函数的第一个参数。因此不要显式地在参数列表中写出。

④赋值运算符函数的形式探究

1.const:

①防修改:避免赋值运算符函数中修改右侧对象的值

②可以绑定右值:能处理通过右值属性的对象进行赋值的情况

2.&:

①返回值类型的&如果去掉:return *this 会 触发拷贝构造函数的第三种调用时机,进行一次多余的复制

②参数中的&如果去掉:实参赋值给形参,会 触发拷贝构造函数的第二种调用时机,进行一次多余的复制

3.返回类型

Q:返回类型是void行不行?

A:不行,若是void类型,则没有返回值。不能进行连续复制,如 pc = pc2 = pc3 //error。

⑤自定义赋值运算符函数



若是自复制,pc = pc,不加判断直接delete,则会出问题。先delete然后访问空指针,造成程序中断。

cpp 复制代码
Computer & operator=(const Cpmputer & rhs){
	cout << "Computer & operator = (const Cpmputer & rhs)" << endl;
	if(this != &rhs){	//1.判断是否是自复制
		delete [] _brand; //2.回收左操作数的数据成员原本管理的堆空间
		_brand = new char[strlen(rhs._brand) + 1]();  //3.深拷贝
		strcpy(_brand, rhs._brand);
		_price = rhs._price;  //以及其他的数据成员完成简单赋值
	}
	return *this;  //4.返回本对象
}

当数据成员申请堆空间时,需要自定义赋值运算符函数进行深拷贝。否则默认的赋值运算符函数是浅拷贝,会造成double free问题。

(3)三合成原则

三合成原则很容易在面试时被问到:

析构函数、拷贝构造函数、赋值运算符函数,如果需要手动定义其中的一个,那么另外两个也需要手动定义。 (即 数据成员有指针,且动态申请了堆空间,编译器默认提供的不够用)

(1)若Computer类:

①不显式定义析构函数,则char* _brand申请的堆空间无法释放,造成内存泄露。

②不显式定义拷贝构造函数和赋值运算符函数,则会浅拷贝,造成double free

(2)为什么四大金刚不提构造函数?因为即使是简单的Pointer类,也需要手动定义构造函数。构造函数的作用是初始化数据成员,默认提供的构造函数无法初始化数据成员。

6.特殊的数据成员

(1)常量数据成员

普通的const常量必须在声明时就初始化,初始化之后就不再允许修改值;const成员初始化后也不再允许修改值。

1.常量数据成员(const数据成员)需在初始化列表中进行初始化。

2.C++11后可以在常量数据成员声明的时候进行初始化,当作默认参数。可以被初始化列表覆盖。

cpp 复制代码
class Point {
public:
	Point(int ix, int iy)
	: _ix(ix)
	, _iy(iy)
	{
		//常量数据成员,只能在初始化列表中进行初始化。不能在这里
	}
private:
	const int _ix; //常量数据成员
	const int _iy = 10; //常量数据成员 + 默认值
};

(2)引用数据成员

1.引用数据成员,必须要在初始化列表 中完成初始化 。(初始化的顺序是数据成员的声明顺序,与初始化列表中的顺序无关)

2.C++11后也可以在声明 时赋默认值 (绑定到已经存在的变量),但可以被初始化列表覆盖。

cpp 复制代码
class Point {
public:
	Point(int ix, int iy)
	: _ix(ix)
	, _iy(iy)
	, _iz(_ix)
	{}
private:
	int _ix;
	int _iy;
	int & _iz;  //引用数据成员
};

3.引用成员需要绑定一个已经存在的、且在这个引用成员的生命周期内始终有效的变量 (对象)。引用成员的生命周期就是对象的生命周期。

【绑定到形参,而形参到构造函数结束后就消亡,则_iz绑定的值会不确定。所以一般传入参数需要加引用。】

(3)对象成员 (成员子对象)

1.无参构造的初始化:

cpp 复制代码
Point(){}
cpp 复制代码
Point() = default;

2.对象子成员: Line对象中包含了两个Point对象


3.在Line类中调用Pointer的函数

4.有对象成员的结构下,构造函数和析构函数的调用顺序

此例子中,创建一个Line类的对象,会首先调用Line的构造函数,在此过程中调用Point的构造函数完成Point类对象成员的初始化;

Line对象销毁时会先调用Line的析构函数,析构函数执行完后,再调用Point的析构函数。

先Line调用析构函数,pt2先调用析构函数,再是pt1

一个Line对象中包含两个Point对象,称为成员子对象。

(4)静态数据成员

1.属于类,而不属于某个对象,静态数据成员被整个类的所有对象共享。

2.静态数据成员存储在全局/静态区,并不占据对象的存储空间。

3.静态成员在访问时可以通过对象访问,也可以直接通过类名::成员名访问。

4.类内声明,类外初始化。(初始化位置一般紧跟着类的定义) (若在类内初始化静态数据成员,则每创建一个新对象都会被刷新)

5.静态数据成员在类外初始化时不能在数据类型前面加static,在数据成员名前面要加上类名+作用域限定符。int Sample::k=0;

6.如果有多条静态数据成员,那么它们的初始化顺序需要与声明顺序一致(规范)

7.特殊的成员函数

(1)静态成员函数

作用:公有的静态成员函数,来访问类的静态成员变量 和 调用类的其他静态成员函数和构造、析构、拷贝构造、赋值运算符函数。

在某一个成员函数的前面加上static关键字,这个函数就是静态成员函数。静态成员函数具有以下特点:

(1)静态成员函数不依赖于某一个对象

(2)静态成员函数可以通过对象调用,但更常见的方式是通过类名加上作用域限定符调用;

(3)静态成员函数没有this指针

(4)静态成员函数中无法直接访问非静态的成员,可以访问静态数据成员或调用静态成员函数 (因为非静态数据成员属于某个对象,需要用this指针来指示是属于哪个对象。而静态成员函数没有this指针)。

(5)静态成员函数可以访问 四大金刚,构造、析构、拷贝构造、赋值运算符函数。因为它们属于类,而不是属于具体的对象。

1.静态成员函数的声明

cpp 复制代码
class Student
{
public:
	static void fun()
	{
		cout << "静态成员函数被调用 : number = " << number << endl;
	}
};

2.静态成员函数的访问:

(1)使用 类名+作用域限定符 访问

cpp 复制代码
	// 通过 类名:: 调用 静态成员函数
	Student::fun();

(2)使用 对象 访问

cpp 复制代码
	// 通过 对象. 调用 静态成员函数
	s.fun();

(2)const成员函数

之前已经介绍了 const 的应用,实际上, const 在类成员函数中还有种特殊的用法。在成员函数的参数列表之后,函数执行体之前加上const关键字,这个函数就是const成员函数。

1.形式:

cpp 复制代码
void func() const {}
cpp 复制代码
class Computer{
public:
    //...
    void print() const{
        cout << "brand:" << _brand << endl;
        cout << "price:" << _price << endl;
    }
    //...
};

2.定义:

将const修饰的"成员函数"称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改 。
const成员函数,就是为了保证不在其内修改数据成员。

this指针的类型:Point * const this。

const成员函数的this指针变成了 const Point * const this

3.作用:
阻止成员函数在内部修改数据成员的值,可以考虑写成const成员函数

4.特点:

(1)const成员函数中,不能修改对象的数据成员;

(2)当编译器发现该函数是const成员函数时,会自动将this指针设置为双重const限定的指针;

例如print函数里,偷偷修改值,用const成员函数阻止这种行为。

cpp 复制代码
//const成员函数:
//this 指针原本是 Point * const this
//const成员函数的this指针变成了const Point * const this
void print () const {
    //_ix = 100;  //改为const成员函数后,无法在其内部修改数据成员的值
    cout << "(" << _ix << "," << _iy << ")" << endl;
}

5.对于指针数据成员:

仅仅限制为常量指针,不能修改指向,但是仍然可以改变指针指向的内容。

若需要限制不让修改指针指向的内容,应该在声明时定义为 const char *

8.对象的组织

(1)const对象

类对象也可以声明为 const 对象,一般来说,能作用于 const 对象的成员函数除了构造函数和析构函

数,就只有 const 成员函数了。因为 const 对象只能被创建、撤销和只读访问,写操作是不允许的。

1.提出原因
如果一个成员函数中确定不会修改数据成员,就把它定义为const成员函数。

cpp 复制代码
const Point pt(1,2);
pt.print();

2.特点

(1)const对象只能调用const成员函数 。(构造函数和析构函数除外)

(2)如果一个成员函数中,不打算修改对象的内容,则可以将这个成员函数设为const成员函数。

const对象只能调用const成员函数,因为安全。

非const对象,既可以调用const成员函数,也可以调用非const成员函数。

const成员函数,就是为了保证不在其内修改数据成员。

(1)对于普通类型的数据成员,const数据成员初始化后不允许修改,在const成员函数中无论是const数据成员还是非const数据成员,都不能修改值;

(2)对于指针类型的数据成员:

const int * p,初始化后不能修改其指向的值,但可以在非const成员函数中修改其指向。(在const成员函数中不能修改指向)

int * p,可以修改值,可以在非const成员函数中修改指向。(在const成员函数中不能修改指向)

(2)指向对象的指针

cpp 复制代码
类名 * 指针名 [=初始化表达式];
cpp 复制代码
Point pt(1, 2);
Point * p1 = nullptr;
Point * p2 = &pt;
Point * p3 = new Point(3, 4);
cpp 复制代码
p2->print();
(*p2).print();

(3)对象数组

对象数组的声明

cpp 复制代码
Point pts[2];

这种格式会自动调用默认构造函数或所有参数都是缺省值的构造函数。

对象数组的初始化(可以在声明时进行初始化)

1.数组存的是副本,会发生拷贝构造函数。

即使是临时 的Point类匿名对象 也是副本。

2.{ }也能创建匿名的Point对象。

(4)堆对象

用new的方式

cpp 复制代码
Point pts[2] = {Point(1, 2), Point(3, 4)};
Point pts[] = {Point(1, 2), Point(3, 4)};
Point pts[5] = {Point(1, 2), Point(3, 4)};
//或者
Point pts[2] = {{1, 2}, {3, 4}};
pts->print();  //(1,2)
(pts + 1)->print(); //(3,4)
Point pts[] = {{1, 2}, {3, 4}};
Point pts[5] = {{1, 2}, {3, 4}};

9.new/delete的工作步骤

若是内置类型,则new和delete的步骤没有构造函数和析构函数这两步。

(1)使用new表达式时发生的三个步骤:(对于自定义类型而言)

①调用operator new 标准库函数申请未类型化的空间 (底层是malloc)

②在该空间上调用该类型的构造函数 初始化对象的数据成员

③返回指向该对象的相应类型的指针

cpp 复制代码
//默认的operator new
void * operator new(size_t sz){
    void * ret = malloc(sz);
	return ret;
}

(2)使用delete表达式时发生的两个步骤:(对于自定义类型而言)

①调用析构函数 ,回收数据成员申请的堆空间

②调用operator delete库函数回收本对象所在的堆空间 (底层是free)

cpp 复制代码
//默认的operator delete
void operator delete(void * p){
    free(p);
}

(3)创建对象的探究:申请堆对象、栈对象

0.现在已知,创建一个类,编译器会默认提供6个公有的成员函数 :构造函数、析构函数、拷贝构造函数、赋值运算符函数、operator new、operator delete

请探究:

1.创建堆对象 需要什么条件?

思路:将创建、销毁对象过程中所调用到的函数一一设为私有,私有的成员函数在类外就无法被直接调用了。

需要公有的operator new、operator delete、构造函数,对析构函数没有要求;在销毁堆对象的时候,才会调用析构函数。

cpp 复制代码
//要求只能创建堆对象,不能创建栈对象:
//将构造函数设为私有
#include <string.h>
#include <iostream> 
using std::cout;
using std::endl;

class Computer
{
public:
    Computer(const char * brand, double price)
    : _brand(new char[strlen(brand) + 1]())
    , _price(price)
    {
        strcpy(_brand, brand);
        cout << "Computer(const char * brand, double price)"  << endl;
    }
    
    void print() const{
        cout << "brand:" << _brand << endl;
        cout << "price:" << _price << endl;
    }

    void destroy(){
        delete this;
    }

private:
    ~Computer(){
        cout << "~Computer()" << endl;
        if(_brand){
            delete [] _brand;
            _brand = nullptr; //安全回收
        }
    }

private:
    char * _brand;
    double _price;
};

void test(){
    //创建栈对象失败,因为析构函数是私有的
    //Computer pc("apple", 20000);
    //pc.print();
    
    //创建堆对象
    Computer * pc  = new Computer("ASUS", 8000);
    pc->print();
    pc->destroy();
    pc = nullptr; //安全回收
}

int main()
{
    test();   
    return 0;
}

①把构造函数私有化,则不可以创建对象,不让new。

②把析构私有化,可以创建对象,可以new。但回收需要手动写一个destory公有接口。:

2.创建栈对象 需要什么条件?

需要公有的构造函数、析构函数 ,对operator new/operator delete没有要求。

若将构造或析构私有,则不允许创建栈对象。

cpp 复制代码
//只能创建栈对象,不能创建堆对象的Computer类
//公有的构造函数和析构函数,私有的 operator new 和 operator delete 函数

#include <string.h>
#include <error.h>
#include <errno.h>
#include <iostream> 
using std::cout;
using std::endl;

class Computer
{
public:
    Computer(const char * brand, double price)
    : _brand(new char[strlen(brand) + 1]())
    , _price(price)
    {
        strcpy(_brand, brand);
        cout << "Computer(const char * brand, double price)" << endl;
    }

    ~Computer(){
        cout << "~Computer()" << endl;
        if(_brand){
            delete [] _brand;
            _brand = nullptr; //安全回收
        }
    }

    void print() const{
        cout << "brand:" << _brand << endl;
        cout << "price:" << _price << endl;
    }

private:
    void * operator new(size_t size){
        cout << "operator new" << endl;
        void * ptr = malloc(size);
        if(ptr == nullptr)  error(1, 0, "operator new malloc");
        return ptr;
    }

    void operator delete(void * pointer){
        cout << "operator delete" << endl;
        free(pointer);
    }

private:
    char * _brand;
    double _price;
};

void test(){
    //创建栈对象
    Computer pc("apple", 20000);
    pc.print();

    //创建堆对象
    //Computer * p = new Computer("ASUS", 18000); //失败,operator new is private

}

int main()
{
    test();   
    return 0;
}

3.根据探究得出的结论,仍以Student类为例,想要实现以下需求,应该怎么做?

①Q:只能生成栈对象 , 不能生成堆对象

A:可以将operator new / operator delete 设为私有。

②Q:只能生成堆对象 ,不能生成栈对象

A:可以将析构函数设为私有。

10.单例模式 (重点*)

单例模式是23种常用设计模式中最简单的设计模式之一,它提供了一种创建对象的方式,确保只有单个对象被创建。这个设计模式主要目的是想在整个系统中只能出现类的一个实例,即一个类只有一个对象。

(1)将单例对象创建在静态区

1.为了只创建一次对象:把 构造函数 和 拷贝构造函数 私有 化。然后设置一个公有接口 getInstance()来创建单例对象。

2.在类外不能创建对象 ,而要调用getInstance(),就必须将getInstance()设置为静态成员函数 ,即 static Point & getInstance(),通过类名来调用 Point::getInstance()。

3.return pt,而返回值pt会在getInstance()调用结束就被销毁。返回值应该要大于函数的生命周期 。因此考虑将getInstance()中声明的pt改为静态对象 。即 static Point pt(1,2);

4.在类外创建单例对象时,用普通的栈对象pt来接收getInstance()的返回值。若用赋值运算符会触发拷贝构造函数的第二种调用时机,而拷贝构造函数已经私有,因此必须用引用对象 & pt来接收getInstance()的返回值。

5.同样,Point getInstance() 的返回值类型也应该改写为 Point & getInstance()。否则 Point & pt1 = Point::getInstance(); 会报错:非const左值无法绑定到右值

cpp 复制代码
//在静态区创建单例对象

#include <iostream> 
using std::cout;
using std::endl;

class Point
{
public:
    //当静态函数多次被调用,静态的局部对象只会被初始化一次
    //第一次调用getInstance(),静态对象会被初始化为一个对象实例
    //在后续的调用中,静态局部对象已经存在,不会再初始化
    //而是直接返回已经初始化的对象实例
    static Point & getInstance(){
        static Point pt(1,2);
        return pt;
    }

    //const成员函数:
    //this 指针原本是 Point * const this
    //const成员函数的this指针变成了const Point * const this
    void print () const {
        //_ix = 100;  //改为const成员函数后,无法在其内部修改数据成员的值
        cout << "(" << _ix << "," << _iy << ")" << endl;
    }

private:
    //1.为了只能创建一个对象,将构造函数和拷贝构造函数私有化
    Point(int x, int y = 0) //默认参数
    :_ix(x)
    ,_iy(y)
    {
        cout << "Point(int,int)" << endl;
    }

	~Point(){
        cout << "~Point()" << endl;
    }
	
	Point(const Point & rhs) = delete;

    Point & operator=(const Point & rhs) = delete;

private:
    int _ix = 10;
    int _iy = 10;
};

void test1(){
    Point & pt1 = Point::getInstance();
    Point & pt2 = Point::getInstance();
    cout << &pt1 << endl;
    cout << &pt2 << endl;
}

int main()
{
    test1();         
    return 0;
}

隐患:单例对象通常所占空间较大,可能会对静态区造成内存压力。

(全局静态区较小,可以优化一下,考虑将单例类放在堆上)

(2)将单例对象创建在堆区 (重点)

1.先将构造函数私有化

2.公有接口getInstance(),堆上申请对象。把返回对象的引用,改为返回指向对象的指针。

输出发现地址是不同的,因为每次调用getInstance()都会new一个新对象。

3.考虑new之前要先判断,加一个Point * _pinstance数据成员。

因为是在静态的成员函数static Point * getInstance()中访问_pinstance,所以需要将 _pinstance声明为静态的数据成员,即static Point * _pinstance,并在类外初始化为nullptr,即 Point * Point::_pinstance = nullptr;

_pinstance改为静态的数据成员:

4.现在不希望多个指针指向同一片内存空间。可能会造成double free。

考虑把析构函数私有化,定义一个公有的接口destroy来销毁。

又不希望调用destroy时创建又销毁的开销。将destroy改为静态。

cpp 复制代码
//定义为静态成员函数,则不需要创建对象也能通过类名调用
static void destroy(){
    if(_pinstance){
        delete _pinstance;
        _pinstance = nullptr; //安全回收
        cout << " delete heap" << endl;
    }
}

单例模式的规范情况:

5.又希望创建的类的数值,可以被修改,即有灵活的值 。(但是无法在初始化时进行赋值,因为getInstance只能走进去一次,因此传参没用)

考虑再定义一个公有的接口 init,专门用来修改单例对象的值。

有的版本的单例模式,采用无参的构造函数

海豹老师推荐使用有参构造来初始化单例对象

6.禁止复制和赋值:从本类中删除拷贝构造函数、赋值运算符函数

7.完整代码

cpp 复制代码
#include <iostream> 
using std::cout;
using std::endl;

class Point
{
public:
    static Point * getInstance(){
        if(_pinstance == nullptr){
            _pinstance = new Point(1,2);
        }
        return _pinstance;
    }
    
    void init(int x, int y){
        _ix = x;
        _iy = y;
    }

    //定义为静态成员函数,则不需要创建对象也能通过类名调用
    static void destroy(){
        if(_pinstance){
            delete _pinstance;
            _pinstance = nullptr; //安全回收
            cout << " delete heap" << endl;
        }
    }

    void print(){
        cout << "(" << _ix << "," << _iy << ")" << endl;
    }

private:
    Point(int x, int y = 0) //默认参数
    :_ix(x)
    ,_iy(y)
    {
        cout << "Point(int,int)" << endl;
    }
    
    ~Point(){
        cout << "~Point()" << endl;
    }
 
    //严格禁止复制和赋值:
    //直接从本类中删除拷贝构造函数和赋值运算符函数
    Point(const Point & rhs) = delete;
    Point & operator=(const Point & rhs) = delete;

private:
    int _ix = 10;
    int _iy = 10;
    static Point * _pinstance;
};
Point * Point::_pinstance = nullptr;

void test0(){
    Point * p1 = Point::getInstance();
    Point * p2 = Point::getInstance();
    cout << p1 << endl;
    cout << p2 << endl; //地址一样,说明确实是单例
    p1->print();
    p2->print();
    //delete p1; //析构被私有,无法delete
    p1 = nullptr; //为什么置空了,还能调用destroy()??这不是对空指针解引用了吗?
    //海豹老师补充:自定义类的空指针,可以访问与对象无关的普通函数和静态成员
    p1->destroy();
    p2->destroy();
}

void test1(){
    Point::getInstance()->print();
    Point::getInstance()->init(3,4);
    Point::getInstance()->print();
    Point::getInstance()->init(5,6);
    Point::getInstance()->print();
    Point::destroy();
    Point::destroy();
    Point::destroy();
    Point::getInstance()->print();
    Point::destroy();
}

int main()
{
    /* test0(); */         
    test1();
    return 0;
}

(3)单例对象的数据成员申请堆空间

1.课上的例子是将Point类改为单例模式,分别创建在静态区和堆空间上。

2.周末作业,是将Computer类改造为单例模式。

要妥善处理char * _brand

cpp 复制代码
#include <string.h>
#include <iostream> 
using std::cout;
using std::endl;

class Computer
{
public:
    static Computer * getInstance(){
        if(_pinstance == nullptr){
            _pinstance = new Computer("ASUS", 8000);
        }
        return _pinstance;
    }

    void init(const char * brand, int price){
        strcpy(_brand, brand);
        _price = price;
    }

    static void destroy(){
        if(_pinstance){
            delete _pinstance;
            _pinstance = nullptr; //安全回收
            cout << "delete heap" << endl;
        }
    }

    void print() const{
        cout << "brand:" << _brand << endl;
        cout << "price:" << _price << endl;
    }

private:
    Computer(const char * brand, double price)
    : _brand(new char[strlen(brand) + 1]())
    , _price(price)
    {
        strcpy(_brand, brand);
    }

    ~Computer(){
        cout << "~Computer()" << endl;
        if(_brand){
            delete [] _brand;
            _brand = nullptr; //安全回收
        }
    }

    Computer(const Computer & rhs) = delete;
    Computer & operator=(const Computer & rhs) = delete;

private:
    char * _brand;
    double _price;
    static Computer * _pinstance;
};
Computer * Computer::_pinstance = nullptr;

void test(){
    /* Computer pc("apple", 20000); */ //不能调用私有的构造函数
    /* pc.print(); */
    Computer * pc1 = Computer::getInstance();
    Computer * pc2 = Computer::getInstance();
    cout << pc1 << endl;
    cout << pc2 << endl;    //地址相同,证明是单例
    
    pc1->print();
    pc2->print();
    
    pc1->init("apple", 20000);
    pc1->print();

    pc2->init("HuaWei", 210000);
    pc2->print();
    
    pc1->destroy();
    pc2->destroy();

}

int main()
{
    test();   
    return 0;
}

(4)单例模式的应用场景

1.有频繁实例化然后销毁的情况,也就是频繁的 new 对象,可以考虑单例模式;

2.创建对象时耗时过多或者耗资源过多,但又经常用到的对象;

3.当某个资源需要在整个程序中只有一个实例时,可以使用单例模式进行管理(全局资源管理)。例如数据库连接池、日志记录器等;

4.当需要读取和管理程序配置文件时,可以使用单例模式确保只有一个实例来管理配置文件的读取和写入操作(配置文件管理);

5.在多线程编程中,线程池是一种常见的设计模式。使用单例模式可以确保只有一个线程池实例,方便管理和控制线程的创建和销毁;

6.GUI应用程序中的全局状态管理:在GUI应用程序中,可能需要管理一些全局状态,例如用户信息、应用程序配置等。使用单例模式可以确保全局状态的唯一性和一致性。

(5)单例模式的完整步骤

1.将构造函数和析构函数私有化

2.设置私有的静态数据成员,并在类外完成初始化(紧接着类的定义结束的位置)

cpp 复制代码
private:
	static 类名 * _pInstance;
}; // end of 类
类名 * 类名::_pInstance = nullptr;	

3.设置一个公有的静态成员函数接口 getInstance(),在堆上申请对象,并返回指向对象的指针。

cpp 复制代码
class Point
{
public:
	static Point * getInstance(){
		if(_pInstance == nullptr){
			_pInstance = new Point(1,2);
		}
		return _pInstance;
	}
cpp 复制代码
class Computer
{
public:
    static Computer * getInstance(){
        if(_pInstance == nullptr){
            _pInstance = new Computer("ASUS", 8000);
        }
        return _pInstance;
    }

4.定义一个公有的静态成员函数接口destroy来销毁单例对象

cpp 复制代码
//定义为静态成员函数,则不需要创建对象也能通过类名调用
static void destroy(){
    if(_pinstance){
        delete _pinstance;
        _pinstance = nullptr; //安全回收
        cout << " delete heap" << endl;
    }
}

5.禁止复制和赋值:从本类中删除拷贝构造函数、赋值运算符函数

cpp 复制代码
类名(const 类名 & rhs) = delete;
类名 & operator=(const 类名 & rhs) = delete;

6.希望创建的类的数值,可以被修改,即有灵活的值

考虑再定义一个公有的接口 init,专门用来修改单例对象的值。

7.完整代码 Singleton.cc

11.C++字符串

(1)C风格字符串

C 风格字符串是以 '\0' (空字符)来结尾的字符数组。

对字符串进行操作的 C 函数定义在头文件 <string.h> 或 < cstring > 中。常用的库函数如下:

海豹老师推荐用C风格的头文件,让读代码的人能一眼看出是引用了C的函数。

c 复制代码
//字符检查函数(非修改式操作)
size_t strlen(const char *str);//返回str的长度,不包括null结束符
//比较lhs和rhs是否相同。lhs等于rhs,返回0; lhs大于rhs,返回正数; lhs小于rhs,返回负数
int strcmp(const char *lhs, const char *rhs);
int strncmp(const char *lhs, const char *rhs, size_t count);
//在str中查找首次出现ch字符的位置;查找不到,返回空指针
char *strchr(const char *str, int ch);
//在str中查找首次出现子串substr的位置;查找不到,返回空指针
char *strstr(const char* str, const char* substr);
//字符控制函数(修改式操作)
char *strcpy(char *dest, const char *src);//将src复制给dest,返回dest
char *strncpy(char *dest, const char *src, size_t count);
char *strcat(char *dest, const char *src);//concatenates two strings
char *strncat(char *dest, const char *src, size_t count);

在使用时,程序员需要考虑字符数组大小的开辟,结尾空字符的处理,使用起来有诸多不便。

cpp 复制代码
void test0()
{
	char str[] = "hello,";
	char * pstr = "world";
	//求取字符串长度
	printf("%d\n", strlen(str));
    
	//字符串拼接
	char * ptmp = (char*)malloc(strlen(str) + strlen(pstr) + 1);
	strcpy(ptmp, str);
	strcat(ptmp, pstr);
	printf("%s\n", ptmp);
    
	//查找子串
	char * p1 = strstr(ptmp, "world");
	free(ptmp);
}

(2)C++字符串

C++ 提供了 std::string (后面简写为 string )类用于字符串的处理。 string 类定义在 C++ 头文件 < string> 中,注意和头文件 < cstring > 区分, < cstring > 其实是对 C 标准库中的 <string.h> 的封装,其定义的是一些对 C 风格字符串的处理函数。

尽管 C++ 支持 C 风格字符串,但在 C++ 程序中最好还是不要使用它们。这是因为 C 风格字符串不仅使用起来不太方便,而且极易引发程序漏洞,是诸多安全问题的根本原因。与 C 风格字符串相比, string不必担心内存是否足够、字符串长度,结尾的空白符等等。 string 作为一个类出现,其集成的成员操作函数功能强大,几乎能满足所有的需求。从另一个角度上说,完全可以把 string 当成是 C++ 的内置数据类型,放在和 int 、 double 等内置类型同等位置上。

std::string标准库提供的一个自定义类类型basic_string,string 类本质上其实是 basic_string 类模板关于 char 型的实例化。使用起来不需要关心内存,直接使用即可。

①string的构造
cpp 复制代码
string();//无参构造函数,生成一个空字符串
string(const char * rhs);//通过c风格字符串构造一个string对象
string(const char * rhs, size_type count);//通过rhs的前count个字符构造一个string对象
string(const string & rhs);//拷贝构造函数
string(const string & rhs,size_t pos, size_t count);//通过string对象的一部分创建新的string
string(size_type count, char ch);//生成一个string对象,该对象包含count个ch字符
string(InputIt first, InputIt last);//以区间 [first, last) 内的字符创建一个string对象

问题:

string str4("world",6)

cout << str4 << endl;

会是什么结果?



迭代器是广义的指针,但返回值并不是指针,而是迭代器

首迭代器:指向首元素的迭代器,begin()

尾后迭代器:指向最后一个元素的下一个位置的迭代器,end()


还可以用拼接的方式构造string

原理:basic_string对加法运算符进行了默认重载(后续会学到),其本质是通过+号进行计算后得到一个basic_string对象,再用这个对象去创建新的basic_string对象

cpp 复制代码
//采取拼接的方式创建字符串
//可以拼接string、字符、C风格字符串
string str3 = str1 + str2;
string str4 = str2 + ',' + str3;
string str5 = str2 + ",world!";
②string的常用操作
cpp 复制代码
const CharT* data() const;
const CHarT* c_str() const; //获取出C++字符串保存的字符串内容,以C风格字符串作为返回值

bool empty() const; //判空

size_type size() const;//获取字符数
size_type length() const;

void push_back(CharT ch);  //字符串结尾追加字符

//在字符串的末尾添加内容,返回修改后的字符串
basic_string& append(size_type count, CharT ch); //添加count个字符
basic_string& append(const basic_string& str);  //添加字符串
basic_string & append(const basic_string& str,  //从原字符串末尾添加str从pos位置的count个字符
                     size_type pos,size_type count);
basic_string& append(const charT* s);      //添加C风格字符串

//查找子串
size_type find( const basic_string& str, size_type pos = 0 ) const;  //从C++字符串的pos位开始查找C++字符串str
size_type find( CharT ch, size_type pos = 0 ) const;      //从C++字符串的pos位开始查找字符ch
size_type find( const CharT* s, size_type pos, size_type count ) const;  //从C++字符串的pos位开始,去查找C字符串的前count个字符

海豹老师:往后拼接建议使用append,使用 + 运算符会创建临时string对象,然后拷贝,效率低。

实践一下string的各种操作,体会C++字符串的遍历。

补充:两个basic_string字符串比较,可以直接使用==等符号进行判断

原理:basic_string对==运算符进行了默认重载(后续会学到)

cpp 复制代码
//非成员函数
bool operator==(const string & lhs, const string & rhs);
bool operator!=(const string & lhs, const string & rhs);
bool operator>(const string & lhs, const string & rhs);
bool operator<(const string & lhs, const string & rhs);
bool operator>=(const string & lhs, const string & rhs);
bool operator<=(const string & lhs, const string & rhs);
③string的遍历
cpp 复制代码
void test(){
    string str1("wangdao");

    //1.通过下标进行遍历
    for(size_t idx = 0; idx < str1.size(); ++idx){
        cout << str1[idx] << " ";
    }
    cout << endl;

    //2.增强for循环 (通常与auto一起使用,auto用来自动推导容器中的元素类型)
    for(auto  ch : str1){ //验证引用符号
        ++ch;
    }
    
    for(auto & ch : str1){
        cout << ch << " ";
    }
    cout << endl;

    //3.迭代器方式进行遍历
    auto it = str1.begin();
    for( ; it != str1.end(); ++it){
        cout << *it << " ";
    }
    cout << endl;
}
1.通过下标遍历

string 对象可以使用下标操作符[]进行访问。

cpp 复制代码
//使用下标遍历
for(size_t idx = 0; idx < str.size(); ++idx){
    cout << str[idx] << " ";
}
cout << endl;

需要注意的是操作符[]并不检查索引是否有效,如果索引超出范围,会引起未定义的行为。而函数 at() 会检查,如果使用 at()的时候索引无效,会抛出 out_of_range 异常

cpp 复制代码
string str("hello");
cout << str.at(4) << endl;  //输出o
cout << str.at(5) << endl;  //运行时抛出异常
2.增强for循环遍历

①针对容器,可以使用增强for循环进行遍历其中的元素。

增强for循环 通常和auto关键字 一起使用,auto关键字用来自动推导 容器中的元素类型

③&引用符号,代表在增强for循环中操作的是元素本身,如果没有引用符号,操作的ch就是元素的副本,str1元素本身没有被改变。

cpp 复制代码
for(auto & ch : str){  //只要是str中的元素,就一一遍历
    cout << ch << " ";
}
cout << endl;
3.迭代器方式进行遍历

①begin函数返回首迭代器(指向首个元素的指针)

②end函数返回尾后迭代器(指向最后一个元素的后一位的指针)

cpp 复制代码
//某容器的迭代器形式为 容器名::iterator
//此处auto推导出来it的类型为string::iterator
auto it = str.begin();
while(it != str.end()){
    cout << *it << " ";
	++it;
}
cout << endl;

12.C++动态数组 vector

C++中,std::vector(向量)是一个动态数组容器,能存放任意类型的数据。

其动态性体现在以下几个方面:

(1)动态大小:std::vector 可以根据需要自动调整自身的大小。它在内部管理一个动态分配的数组,可以根据元素的数量进行自动扩容或缩减。当元素数量超过当前容量时,std::vector 会重新分配内存,并将元素复制到新的内存位置。这使得 std::vector 能够根据需要动态地增长或缩小容量,而无需手动管理内存。

(2)动态插入和删除:std::vector 允许在任意位置插入或删除元素,而不会影响其他元素的位置。当插入新元素时,std::vector 会自动调整容量,并将后续元素向后移动以腾出空间。同样地,当删除元素时,std::vector 会自动调整容量,并将后续元素向前移动以填补空缺。

(3)动态访问:std::vector 提供了随机访问元素的能力。可以通过索引直接访问容器中的元素,而不需要遍历整个容器。这使得对元素的访问具有常数时间复杂度(O(1)),无论容器的大小如何。

(1)vector的构造

vector常用的几种构造形式:vector<变量类型> 变量名

①初始化为空:无参构造,仅指明vector存放元素的种类,没有存放元素,无内容;

cpp 复制代码
vector <int> vec1;

②count个默认值:只写个数,不写具体值,则每个元素的值为该类型对应的默认值0

cpp 复制代码
vector<long> vec2(10); //存放10个0

③count个value值:传入两个参数,第一个参数为vetor存放元素的数量,第二个参数为每个元素的值(相同);

cpp 复制代码
vector<long> vec3(10,20); //存放10个20

④直接使用初始化列表(大括号)赋初值,直接指明存放的所有元素的值

cpp 复制代码
vector<int> vec4{1,2,3,4,5,6,7};

⑤拷贝构造其他vector

cpp 复制代码
vector<int> vec5(vec4);

⑥迭代器方式初始化vector,传入两个迭代器作为参数,第一个为首迭代器,第二个为尾后迭代器;迭代器范围,左闭右开。

cpp 复制代码
vector<int> vec6(vec5.begin(),vec5.end() - 3);
cpp 复制代码
int arr[10] = {1,3,5,7,9,2,4,6,8,10};
vector<int> vec7(arr, arr+10);

举例:

cpp 复制代码
void test(){
    int arr[5] = {1, 2, 3, 4, 5};
    
    //vector<int>代表一个特点的类
    //vector可以代表一类类,vector是类模板实现的
    //1.无参构造的vector是空的
    vector<int> nums;
    for(auto & ele : nums){
        cout << ele << " ";
    }
    cout << endl;

    //2.传一个参数,规定了vector的个数,默认初始化为类型对应的默认值
    vector<long> nums2(10); //存放10个0,类型对应的默认值
    for(auto & ele : nums2){
        cout << ele << " ";
    }
    cout << endl;

    //3.第一个参数是个数,第二个参数是初始化的值
    vector<long> nums3(10, 6);
    for(auto & ele : nums3){
        cout << ele << " ";
    }
    cout << endl;
    
    //4.大括号形式
    vector<int> nums4 {1,2,3,4,5,6,7,8,9,10};
    for(auto & ele : nums4){
        cout << ele << " ";
    }
    cout << endl;

    //5.迭代器
    vector<int> nums5(nums4);
    for(auto & ele : nums5){
        cout << ele << " ";
    }
    cout << endl;

    vector<int> nums6(nums4.begin(), nums4.end()-2);
    for(auto & ele : nums6){
        cout << ele << " ";
    }
    cout << endl;

    vector<int> nums7(arr, arr+5);
    for(auto & ele : nums7){
        cout << ele << " ";
    }
    cout << endl;
}

(2)vector的常用操作

cpp 复制代码
iterator begin();  //返回首位迭代器
iterator end();    //返回尾后迭代器

bool empty() const; //判空

size_type size() const; //返回容器中存放的元素个数
size_type capacity() const; //返回容器容量(最多可以存放元素的个数)

void push_back(const T& value); //在最后一个元素的后面再存放元素

void pop_back(); //删除最后一个元素
void clear(); //清空所有元素,但不释放空间
void shrink_to_fit();  //释放多余的空间(可以存放元素但没有存放运算的空间)

void reserve(size_type new_cap);//申请空间,不存放元素(占位,防频繁扩容)

①有参构造 vector<int> nums(10000); 不仅开辟了存放元素的空间,还对所有元素进行了初始化(若vector存放的是自定义类,则每次初始化都调用构造函数)。效率低。

②而无参构造 vector nums2(); 没有开辟存放元素的空间。若是每次都push_back(),则要频繁进行动态扩容。

此时就可以使用reserve()函数:nums2.reserve(10000),size是0,capacity是10000

(3)vector的遍历

①下标方式遍历

②增强for循环 (范围for循环)

③迭代器

cpp 复制代码
//2.vector的遍历
void test2(){
    vector<int> vec = {1,2,3,4,5};
    //1.下标方式遍历
    for(size_t idx = 0; idx < vec.size(); idx++){
        cout << vec[idx] << " ";
    }
    cout << endl;

    //2.增强for循环遍历 (范围for循环)
    for(auto & ele : vec){
        cout << ele << " ";
    }
    cout << endl;

    //3.迭代器方式遍历
    for(auto it = vec.begin(); it != vec.end(); ++it){
        cout << *it << " ";
    }
    cout << endl;
}

(4)vector的动态扩容

第一次初始化时,capacity == size,后续追加元素,vector会自动扩容。

当vector存放满后,仍然追加存放元素,vector会进行自动扩容。

①GCC中的vector是2倍的容量扩容机制:当vector存满后再添加新的元素,容量就会变成2倍,把新的元素存入其中。

②VS上是1.5倍的扩容。

------ 很多技术上具体的实现,在不同的平台上细节不同。C++标准给出功能的要求,各个编译器只需要实现此功能。

其工作步骤如下:

(1)开辟空间

(2)Allocator分配(后面STL阶段学习)

(3)复制,再添加新元素

(4)回收原空间

(5)vector的底层实现 (重点*)

vector的大小是24字节,底层实现是三个指针:

cpp 复制代码
vector<int> nums(5,8);
cout << "nums's size: " << nums.size() << endl;
cout << "nums's capacity: " << nums.capacity() << endl;
cout << sizeof(nums) << endl; //vector对象的大小是24字节,三个指针

_start

_finish

_end_of_storage

可以推导出:

①size() : _finish - _start

②capacity(): _end_of_storage - start

源码:/usr/include/C++/7

相关推荐
金士顿3 小时前
MFC 文档模板 每个文档模板需要实例化吧
c++·mfc
人才程序员6 小时前
QML z轴(z-order)前后层级
c语言·前端·c++·qt·软件工程·用户界面·界面
w(゚Д゚)w吓洗宝宝了6 小时前
C vs C++: 一场编程语言的演变与对比
c语言·开发语言·c++
小老鼠不吃猫8 小时前
C++点云大文件读取
开发语言·c++
姚先生978 小时前
LeetCode 35. 搜索插入位置 (C++实现)
c++·算法·leetcode
CoderCodingNo9 小时前
【GESP】C++二级考试大纲知识点梳理, (4)流程图
开发语言·c++·流程图
小小unicorn9 小时前
【C++初阶】STL详解(十三)—— 用一个哈希表同时封装出unordered_map和unordered_set
java·c++·散列表
慕羽★9 小时前
详细介绍如何使用rapidjson读取json文件
linux·c++·windows·json·file·param·rapidjson
大梦百万秋10 小时前
C++中的虚拟化:通过虚拟机管理模拟服务器虚拟化
服务器·开发语言·c++
shentuyu木木木(森)10 小时前
入门STL(map/multiset)
开发语言·数据结构·c++·算法·map·multiset