类是创建对象的模板,一个类可以创建多个对象,每个对象都是类类型的一个变量;创建对象的过程也叫类的实例化。每个对象都是类的一个具体实例(Instance),拥有类的成员变量和成员函数。
与结构体一样,类只是一种复杂数据类型的声明,不占用内存空间。

而对象是类这种数据类型的一个变量,或者说是通过类这种数据类型创建出来的一份实实在在的数据,所以占用内存空间。

注意在类定义的最后有一个分号;,它是类定义的一部分,表示类定义结束了,不能省略。

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

所以比较好的做法是在构造函数时进行成员变量的初始化。

使用对象指针

  1. #include <iostream>
  2. using namespace std;
  3. //类通常定义在函数外面
  4. class Student{
  5. public:
  6. //类包含的变量
  7. char *name;
  8. int age;
  9. float score;
  10. //类包含的函数
  11. void say(){
  12. cout<<name<<"的年龄是"<<age<<",成绩是"<<score<<endl;
  13. }
  14. };
  15. int main(){
  16. //创建对象
  17. Student stu;
  18. stu.name = "小明";
  19. stu.age = 15;
  20. stu.score = 92.5f;
  21. stu.say();
  22. return 0;
  23. }

上面代码中创建的对象 stu 在栈上分配内存,需要使用&获取它的地址,

  1. Student stu;
  2. Student *pStu = &stu;

pStu 是一个指针,它指向 Student 类型的数据,也就是通过 Student 创建出来的对象。
当然,你也可以在堆上创建对象,这个时候就需要使用前面讲到的new关键字

栈内存:栈内存首先是一片内存区域,存储的都是局部变量,凡是定义在方法中的都是局部变量(方法外的是全局变量),for循环内部定义的也是局部变量,是先加载函数才能进行局部变量的定义,所以方法先进栈,然后再定义变量,变量有自己的作用域,一旦离开作用域,变量就会被释放。栈内存的更新速度很快,因为局部变量的生命周期都很短。

  1. 堆内存:存储的是数组和对象(其实数组就是对象),凡是new建立的都是在堆中,堆中存放的都是实体(对象),实体用于封装数据,而且是封装多个(实体的多个属性),如果一个数据消失,这个实体也没有消失,还可以用,所以堆是不会随时释放的,但是栈不一样,栈里存放的都是单个变量,变量被释放了,那就没有了。堆里的实体虽然不会被释放,但是会被当成垃圾,垃圾回收机制不定时的收取。

image.png
在栈上创建出来的对象都有一个名字,比如 stu,使用指针指向它不是必须的。但是通过 new 创建出来的对象就不一样了,它在堆上分配内存,没有名字,只能得到一个指向它的指针,所以必须使用一个指针变量来接收这个指针,否则以后再也无法找到这个对象了,更没有办法使用它。
也就是说,使用 new 在堆上创建出来的对象是匿名的,没法直接使用,必须要用一个指针指向它,再借助指针来访问它的成员变量或成员函数。

Qt中的窗口好像都是new方法直接创建在堆上的

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

有了对象指针后,可以通过箭头->来访问对象的成员变量和成员函数,这和通过结构体指针来访问它的成员类似,请看下面的示例:

虽然在一般的程序中无视垃圾内存影响不大,但记得 delete 掉不再使用的对象依然是一种良好的编程习惯。

通过对象名字访问成员使用点号.,通过对象指针访问成员使用箭头->,这和结构体非常类似。

类的成员变量和成员函数

这里给一个内链函数定义在类外部的例子

class Student{
public:
    char *name;
    int age;
    float score;
    void say();  //内联函数声明,可以增加 inline 关键字,但编译器会忽略
};
//函数定义
inline void Student::say(){
    cout<<name<<"的年龄是"<<age<<",成绩是"<<score<<endl;
}

这种在类体外定义 inline 函数的方式,必须将类的定义和成员函数的定义都放在同一个头文件中(或者同一个源文件中),否则编译时无法进行嵌入(将函数代码的嵌入到函数调用出)

成员访问权限和类的封装

C++通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。所谓访问权限,就是你能不能使用该类中的成员。

另外还有一个关键字 protected,声明为 protected 的成员在类外也不能通过对象访问,但是在它的派生类内部可以访问,这点我们将在后续章节中介绍,现在你只需要知道 protected 属性的成员在类外无法访问即可。

C++中的内存模型

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

C++构造函数

C++中,有一种特殊的成员函数,它的名字和类名相同,没有返回值,不需要用户显式调用(用户也不能调用),而是在创建对象时自动执行。这种特殊的成员函数就是构造函数(Constructor)。

构造函数没有返回值,因为没有变量来接收返回值,即使有也毫无用处,这意味着:

  • 不管是声明还是定义,函数名前面都不能出现返回值类型,即使是 void 也不允许;
  • 函数体中不能有 return 语句。

构造函数的重载

和普通成员函数一样,构造函数是允许重载的。一个类可以有多个重载的构造函数,创建对象时根据传递的实参来判断调用哪一个构造函数。

构造函数的调用是强制性的,一旦在类中定义了构造函数,那么创建对象时就一定要调用,不调用是错误的。如果有多个重载的构造函数,那么创建对象时提供的实参必须和其中的一个构造函数匹配;反过来说,创建对象时只有一个构造函数会被调用。

C++构造函数初始化列表

使用构造函数初始化列表并没有效率上的优势,仅仅是书写方便,尤其是成员变量较多时,这种写法非常简单明了。

class Student{
public:
    Student():name_="Nobody"{}
private:
    char* name_;
};

注意,成员变量的初始化顺序与初始化列表中列出的变量的顺序无关,它只与成员变量在类中声明的顺序有关。

obj 在栈上分配内存,成员变量的初始值是不确定的。

构造函数初始化列表还有一个很重要的作用,那就是初始化 const 成员变量。初始化 const 成员变量的唯一方法就是使用初始化列表。

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

C++析构函数

创建对象时系统会自动调用构造函数进行初始化工作,同样,销毁对象时系统也会自动调用一个函数来进行清理工作,例如释放分配的内存、关闭打开的文件等,这个函数就是析构函数。
析构函数(Destructor)也是一种特殊的成员函数,没有返回值,不需要程序员显式调用(程序员也没法显式调用),而是在销毁对象时自动执行。构造函数的名字和类名相同,而析构函数的名字是在类名前面加一个~符号。

注意:析构函数没有参数,不能被重载,因此一个类只能有一个析构函数。如果用户没有定义,编译器会自动生成一个默认的析构函数。

#include <iostream>

class VLA{
    public:
    VLA(int len); // 构造函数
    ~VLA(); // 析构函数

    void input(); // 从terminal输入数组元素
    void show(); // 显示数组元素

    private:
    int *at(int i); // 获取第i个元素的指针

    const int len_; // const修饰,只能用初始化列表
    int *arr_ptr_; // 数组指针
    int *ptr_;    // 指向数组第i个元素的指针
};

VLA::VLA(int len): len_{len}{
    if (len>0){
    arr_ptr_ = new int[len]; // 分配堆空间
    }
    else{
    arr_ptr_ = NULL;
    }
}

VLA::~VLA(){
    delete[] arr_ptr_; // 释放堆空间
}

在delete xx对象的时候触发。
上述程序中,VLA类的实例化,需要在堆空间上,所以需要一个指针

VLA *parr = new VLA(10);
delete parr;

析构函数在对象被销毁时调用,而对象的销毁时机与它所在的内存区域有关。

在所有函数之外创建的对象是全局对象,它和全局变量类似,位于内存分区中的全局数据区,程序在结束执行时会调用这些对象的析构函数。

在函数内部创建的对象是局部对象,它和局部变量类似,位于栈区,函数执行结束时会调用这些对象的析构函数。

new 创建的对象位于堆区,通过 delete 删除时才会调用析构函数;如果没有 delete,析构函数就不会被执行。

#include <iostream>
#include <string>

class Demo{
  public:
  Demo(string s);
  ~Demo();
  private:
  string s_;
};

Demo::Demo(string s): s_{s}{}

Demo::~Demo(){std::cout << "析构" << s_ <<std::endl;}

void func(){
  // 局部对象,随着函数执行结束就销毁了
    Demo obj1("1");
}
// 全局对象,只有main程序结束才会销毁
Demo obj2("2");

int main(){
    // 局部对象,main函数结束执行析构
  Demo obj3("3");
  // 堆空间上 new 创建的对象,没有delete pobj4就不会执行析构
  Demo *pobj4 = new Demo("4");
  func();
  std::cout << "main" << std::endl;
  return 0;
}

C++ static静态成员变量

有时候我们希望在多个对象之间共享数据,对象 a 改变了某份数据后对象 b 可以检测到。共享数据的典型使用场景是计数,以前面的 Student 类为例,如果我们想知道班级中共有多少名学生,就可以设置一份共享的变量,每次创建对象时让该变量加 1。
C++中,我们可以使用静态成员变量来实现多个对象共享数据的目标。静态成员变量是一种特殊的成员变量,它被关键字static修饰,例如:

class Student{
    public:
    Student(char* name, int age, float score);
    void show();
    static int total_;

    private:
    char *name_;
    int age_;
    float score_;
};

static 成员变量必须在类声明的外部初始化,具体形式为:

type class::name = value;

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

int Student::total_ = 1;

静态成员变量在初始化时不能再加 static,但必须要有数据类型。被 private、protected、public 修饰的静态成员变量都可以用这种方式初始化。
注意:static 成员变量的内存既不是在声明类时分配,也不是在创建对象时分配,而是在(类外)初始化时分配。反过来说,没有在类外初始化的 static 成员变量不能使用。
访问方式

// 通过类访问 static 成员变量
Student::total_ = 10;
// 通过对象来访问 static 成员变量
Student stu("petwan", 12, 92.5F);
stu.total_ = 20;
// 通过对象指针访问 static 成员变量
Student *pstu = new Student("peter",12,50.0F);
pstu -> total_ = 20;

注意:static 成员变量不占用对象的内存,而是在所有对象之外开辟内存,即使不创建对象也可以访问。

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

python中则是在class定义之后,def init之前引入共享的数据。

class Student:
    total: int = 0

    def __init__(self, name, score) -> None:
        self.name = name
        self.score = score


A = Student(name="petwan", score=12.0)
Student.total = 1
B = Student(name="peter", score=13.0)

print(B.total)

C++ static 静态成员函数

在类中,static 除了可以声明静态成员变量,还可以声明静态成员函数。

普通成员函数可以访问所有成员(包括成员变量和成员函数),静态成员函数只能访问静态成员。

编译器在编译一个普通成员函数时,会隐式地增加一个形参 this,并把当前对象的地址赋值给 this,所以普通成员函数只能在创建对象后通过对象来调用,因为它需要当前对象的地址。

而静态成员函数可以通过类来直接调用,编译器不会为它增加形参 this,它不需要当前对象的地址,所以不管有没有创建对象,都可以调用静态成员函数。

就是python中的@staticmethod装饰器

静态成员函数与普通成员函数的根本区别在于:普通成员函数有 this 指针,可以访问类中的任意成员;而静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)。

C++中,静态成员函数的主要目的是访问静态成员。

C++ const成员变量和成员函数

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

需要强调的是,必须在成员函数的声明和定义处同时加上 const 关键字。
char getname() const和char getname()是两个不同的函数原型,如果只在一个地方加 const 会导致声明和定义处的函数原型冲突。

最后再来区分一下 const 的位置:

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

const 对象

需要主要的是,stu、pstu 分别是常对象以及常对象指针,它们都只能调用 const 成员函数,调用其他非const的成员函数,则会报错。

C++友元函数和友元类

借助友元(friend),可以使得其他类中的成员函数以及全局范围内的函数访问当前类的 private 成员。

C++ class和struct

在C++中,struct 类似于 class,既可以包含成员变量,又可以包含成员函数。
C++中的 struct 和 class 基本是通用的,唯有几个细节不同:

  • 使用 class 时,类中的成员默认都是 private 属性的;而使用 struct 时,结构体中的成员默认都是 public 属性的。
  • class 继承默认是 private 继承,而 struct 继承默认是 public 继承(《C++继承与派生》一章会讲解继承)。
  • class 可以使用模板,而 struct 不能(《模板、字符串和异常》一章会讲解模板)。

在编写C++代码时,我强烈建议使用 class 来定义类,而使用 struct 来定义结构体,这样做语义更加明确。

C++ string

string s = "http://c.biancheng.net";
int len = s.length();
cout<<len<<endl;
#对string中的元素进行访问,采用s[index]的形式
#include <iostream>
#include <string>
using namespace std;
int main(){
    string s = "1234567890";
    for(int i=0,len=s.length(); i<len; i++){
        cout<<s[i]<<" ";
    }
    cout<<endl;
    s[5] = '5';
    cout<<s<<endl;
    return 0;
}

string的拼接可以直接使用+
string& insert (size_t pos, const string& str);

#include <iostream>
#include <string>
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;
}