ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

C++:类和对象

C++:类和对象

目录

1.类的定义

一、类定义格式

二、访问限定符

三、类域

2.实例化

一、实例化概念

二、对象大小

内存对齐规则:

3.this指针

4.类的默认成员函数

一、构造函数

构造函数的特点:

注意:

二、析构函数

析构函数特点:

三、拷贝构造函数

拷贝构造函数特点:

四、赋值运算符重载

五、取地址运算符重载(operator &)

六、const取地址运算符重载(operator &() const)

5.再探构造函数

6.static成员

7.友元

8.内部类

9.匿名对象


1.类的定义

一、类定义格式

  • class为定义类的关键字,Stack为类的名字,{}中为类的主体。类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的⽅法或者成员函数。
  • C++中struct也可以定义类,C++兼容C中struct的⽤法,同时struct升级成了类,明显的变化struct中可以定义函数,⼀般情况下我们还是推荐⽤class定义类。
  • 定义在类⾯的成员函数默认为inline。
class A { public: void Init() { _a = 10; } void Print() { cout << _a << endl; } private: int _a; };

二、访问限定符

  • public修饰的成员在类外可以直接被访问;protected和private修饰的成员在类外不能直接被访问,protected和private是⼀样的。
  • 访问权限作⽤域从该访问限定符出现的位置开始直到下⼀个访问限定符出现时为⽌,如果后⾯没有 访问限定符,作⽤域就到 }即类结束。
  • class定义成员没有被访问限定符修饰时默认为private,struct默认为public。
  • ⼀般成员变量都会被限制为private/protected,需要给别⼈使⽤的成员函数会放为public。
class A { public: void Init() { _a = 10; } void Print() { cout << _a << endl; } private: int _a; }; int main() { A a; a.Init(); a.Print(); //a._a++;//运行时会报错,因为在类外面无法访问private中的成员 return 0; }

运行结果:

10

三、类域

  • 用class/struct大括号{}包裹起来的区域,就是类域。
  • 类定义了⼀个新的作⽤域,类的所有成员都在类的作⽤域中,在类体外定义成员时,需要使⽤ :: 作用域操作符指明成员属于哪个类域。
class A { public: void Init(); void Print();//声明 private: int _a; }; void A::Init()//A:: 表示Init属于A这个类域 { _a = 10; } void A::Print()//定义 { cout << _a << endl;

2.实例化

一、实例化概念

  • ⽤类类型在物理内存中创建对象的过程,称为类实例化出对象。
  • class Date :只是一个类型定义,编译时只记录类域,成员,不分配对象内存
class Date { public: void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "/" << _month << "/" << _day << endl; } private: //这里只是声明,没有开空间 int _year; int _month; int _day; }; int main() { Date d1;//Date类实例化出对象d1 d1.Init(2026, 10, 20); d1.Print(); return 0; }

二、对象大小

  • 类实例化之后,对象占用内存字节数,只算非静态成员变量
  • 成员函数、静态成员变量不算在对象大小里面,放在静态区里面,不属于对象内存
  • 统计时只统计非static成员变量,并遵循内存对齐规则。
内存对齐规则:
第⼀个成员在与结构体偏移量为0的地址处。
  • 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
  • 注意:对⻬数 = 编译器默认的⼀个对⻬数 与 该成员⼤⼩的较⼩值。
  • VS中默认的对⻬数为8
  • 结构体总⼤⼩为:最⼤对⻬数(所有变量类型最⼤者与默认对⻬参数取最⼩)的整数倍。
  • 如果嵌套了结构体的情况,嵌套的结构体对⻬到⾃⼰的最⼤对⻬数的整数倍处,结构体的整体⼤⼩
  • 就是所有最⼤对⻬数(含嵌套结构体的对⻬数)的整数倍。
class Date { public: void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "/" << _month << "/" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1; cout << sizeof(d1) << endl;//计算d1的大小 return 0; }

运行结果:

12

3.this指针

  • 编译器编译后,类的成员函数默认都会在形参第⼀个位置,增加⼀个当前类类型的指针,叫做this 指针。⽐如Date类的Init的真实原型为, void Init(Date* const this, int year, int month, int day);
  • 类的成员函数中访问成员变量,本质都是通过this指针访问的,如Init函数中给_year赋值,this- >_year = year;
  • C++规定不能在实参和形参的位置显⽰的写this指针(编译时编译器会处理),但是可以在函数体内显 ⽰使⽤this指针。
class Date { public: void Init(int year, int month, int day) { //this = nullptr;//编译时会报错,因为这里this的类型是Date cont * this this->_year = year; this->_month = month; this->_day = day; } void Print() { cout << _year << "/" << _month << "/" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1; d1.Init(2026, 1, 1); d1.Print(); return 0; }

4.类的默认成员函数

  • 默认成员函数就是⽤⼾没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数。⼀个类,我们不写的情况下编译器会默认⽣成以下6个默认成员函数;

一、构造函数

  • 构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并 不是开空间创建对象(我们常使⽤的局部对象是栈帧创建时,空间就开好了),而是对象实例化时初始化对象。
构造函数的特点:
  1. 函数名与类名相同。
  2. 无返回值。
  3. 对象实例化时系统会自动调用对应的构造函数。
  4. 构造函数可以重载。
  5. 如果类中没有显式定义构造函数,则C++编译器会自动⽣成⼀个无参的默认构造函数,⼀旦用户显式定义编译器将不再⽣成。
  6. 无参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的构造函数,都叫做默认构造函 数。但是这三个函数有且只有⼀个存在,不能同时存在。⽆参构造函数和全缺省构造函数虽然构成 函数重载,但是调⽤时会存在歧义。
class Date { public: Date()//构造函数 { _year = 1; _month = 1; _day = 1; } Date(int year, int month, int day)//函数重载 { _year = year; _month = month; _day = day; } void Print() { cout << _year << "/" << _month << "/" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1; Date d2(2026,1,1); d1.Print(); d2.Print(); return 0; }

运行结果:

1/1/1 2026/1/1
注意:
  • 调用无参构造时“不要写括号!”,写了括号时就相当于时一个函数声明,编译时会报错。

//Date d3();//函数声明:Date d3(void);
  • 我们不写,编译器默认⽣成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。
class Date { public: void Print() { cout << _year << "/" << _month << "/" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1; d1.Print(); return 0; }

运行结果:

-858993460/-858993460/-858993460
  • 对于自定义类型成员变量,要求调⽤这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错。

二、析构函数

  • 析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,比如局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。
析构函数特点:
  • 析构函数名是在类名前加上字符 ~。
  • 无参数无返回值。
  • 一个类只能有一个析构函数。若未显示定义,系统会自动生成默认的析构函数。
  • 对象生命周期结束时,系统会自动调用析构函数。
  • 跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,自定类型成员会调用他的析构函数。
  • 还需要注意的是我们显⽰写析构函数,对于自定义类型成员也会调用他的析构,也就是说⾃定义类型成员无论什么情况都会⾃动调用析构函数。
  • 如果类中没有申请资源时,析构函数可以不写,直接使⽤编译器⽣成的默认析构函数,如Date。
class stack { public: stack(int n = 4)//构造函数 { int* tmp = (int*)malloc(sizeof(int) * n); if (tmp == nullptr) { perror("malloc"); exit(1); } arr = tmp; top = 0; capacity = n; } ~stack()//析构函数 { cout<<"~stack"<<endl; free(arr); arr = nullptr; top = capacity = 0; } private : int* arr; int top; int capacity; }; int main() { stack st; return 0; }

运行结果:

~stack

三、拷贝构造函数

  • 拷贝构造函数,是一种特殊的构造函数,用一个已经存在的同类对象,去初始化新建对象
拷贝构造函数特点:
  • 拷贝构造函数的第⼀个参数必须是类类型对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。 拷贝构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引 用,后面的参数必须有缺省值。
  • 拷贝构造函数是构造函数的⼀个重载。
  • C++规定自定义类型对象进⾏拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调⽤拷贝构造完成。
  • 若未显式定义拷贝构造,编译器会⽣成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对⾃定义类型成员变量会调用他的拷贝构造。
class Date { public: Date()//构造函数 { _year = 1; _month = 1; _day = 1; } Date(int year, int month, int day)//函数重载 { _year = year; _month = month; _day = day; } Date(Date& d)//拷贝构造 { _year = d._year; _month = d._month; _day = d._day; } void Print() { cout << _year << "/" << _month << "/" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1(2026, 1, 1); Date d2 = d1; Date d3(d1); d1.Print(); d2.Print(); d3.Print(); return 0; }

运行结果:

2026/1/1 2026/1/1 2026/1/1

注意:因为Date类成员变量是内置类型,当我们不写拷贝构造的时候,会默认使用系统的拷贝构造,也就是完成浅拷贝(一个字节一个字节的拷贝),运行出的结果和写了拷贝构造的效果是一样的。

自定义类型的拷贝构造:

class stack { public: stack(int n = 4) { int* tmp = (int*)malloc(sizeof(int) * n); if (tmp == nullptr) { perror("malloc"); exit(1); } arr = tmp; top = 0; capacity = n; } ~stack() { free(arr); arr = nullptr; top = capacity = 0; } stack(stack& st) { arr = (int*)malloc(sizeof(int) * st.capacity); if (arr == nullptr) { perror("malloc"); exit(1); } memcpy(arr, st.arr, sizeof(int) * st.top); top = st.top; capacity = st.capacity; } void Push(int n) { if (top == capacity) { int newcapacity = 2 * capacity; int* tmp = (int*)realloc(arr, sizeof(int) * newcapacity); if (tmp == nullptr) { perror("realloc"); exit(1); } capacity = newcapacity; arr = tmp; } arr[top++] = n; } void Print() { for (int i = 0; i < top; i++) { cout << arr[i] << " "; } cout << endl; } private : int* arr; int top; int capacity; }; int main() { stack st; st.Push(1); st.Push(2); st.Push(3); st.Push(4); st.Push(5); stack st1 = st; st.Print(); st1.Print(); return 0; }

运行结果:

1 2 3 4 5 1 2 3 4 5

四、赋值运算符重载

  • 只能作为类的成员函数重载,不能是全局函数。
  • = 运算符重载强制要求成员函数,因为它需要隐式this指针,左操作数必须是类对象。
  • 触发条件是两个对象已经存在,进行对象赋值。
class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } Date& operator = (const Date& d1) { if (this != &d1) { _year = d1._year; _month = d1._month; _day = d1._day; } return *this; } void Print() { cout << _year << "/" << _month << "/" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1(2026, 1, 1); Date d2(2026, 2, 2); d1.Print(); d2.Print(); d2 = d1;//赋值重载 //Date d3 = d1;//这是拷贝构造!不是赋值重载! d2.Print(); return 0; }

五、取地址运算符重载(operator &)

  • 对非const对象取地址,返回当前对象的this指针
Date* operator&() { return this; //return nullptr//返回空值 }

六、const取地址运算符重载(operator &() const)

  • 对const对象取地址。
  • 成员函数后面的const,表示这个成员函数不会修改对象成员,才能被 const 对象调用。
Date* operator&() const { return this; //return nullptr//返回空值 }

5.再探构造函数

  • 之前我们实现构造函数时,初始化成员变量主要使⽤函数体内赋值,构造函数初始化还有⼀种方式,就是初始化列表,初始化列表的使⽤⽅式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成员列表,每个"成员变量"后⾯跟⼀个放在括号中的初始值或表达式。
  • 每个成员变量在初始化列表中只能出现⼀次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。
  • 引用成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进⾏初始化,否则会编译报错。
  • 初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的的先后顺序无关。建议声明顺序和初始化列表顺序保持⼀致。
  • C++11⽀持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显⽰在初始化列表初始化的成员使用的。
class Date { public: Date() :_year(2026) { } void Print() { cout << _year << "/" << _month << "/" << _day << endl; } private: int _year ; int _month = 1; int _day = 1; }; int main() { Date d1; d1.Print(); return 0; }

运行结果:

2026/1/1

6.static成员

  • ⽤static修饰的成员变量,称之为静态成员变量,静态成员变量⼀定要在类外进行初始化。
  • 静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。
  • 用static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。
  • 非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
  • 突破类域就可以访问静态成员,可以通过类名::静态成员 或者 对象.静态成员 来访问静态成员变量和静态成员函数。
  • 静态成员也是类的成员,受public、protected、private 访问限定符的限制。
  • 静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表。
class A { public: A() { _count++; } ~A() { _count++; } A(A& a) { _count++; } static int Getcount() { return _count; } private: static int _count;//类里面声明 }; int A::_count = 0;//类外面初始化 int main() { A a; A b; A c = b; cout << A::Getcount() << endl; return 0; }

运行结果:

3

7.友元

  • 友元提供了⼀种突破类访问限定符封装的⽅式,友元分为:友元函数和友元类,在函数声明或者类声明的前⾯加friend,并且把友元声明放到⼀个类的里面。
  • 外部友元函数可访问类的私有和保护成员,友元函数仅仅是⼀种声明,他不是类的成员函数。
  • 友元函数可以在类定义的任何地⽅声明,不受类访问限定符限制。
  • ⼀个函数可以是多个类的友元函数。
  • 友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员。
  • 友元类的关系是单向的,不具有交换性,比如A类是B类的友元,但是B类不是A类的友元。
class B;//// 前置声明,否则A的友元函数声明编译器不认识B class A { public: friend void func(const A& a, const B& b);//友元声明 private: int _a = 1; int _b = 2; }; class B { public: friend void func(const A& a, const B& b);//友元声明 private: int _c = 3; int _d = 4; }; void func(const A& a, const B& b) { cout << a._a << endl; cout << b._c << endl; } int main() { A a; B b; func(a, b); return 0; }

运行结果:

1 3
class A { public: friend class B;//友元声明 private: int _a = 1; int _b = 2; }; class B { public: void func(const A& a) { cout << a._a << endl; cout << _d << endl; } private: int _c = 3; int _d = 4; }; int main() { A a; B b; b.func(a); return 0; }

运行结果:

1 4

8.内部类

  • 如果⼀个类定义在另⼀个类的内部,这个内部类就叫做内部类。内部类是⼀个独⽴的类,跟定义在全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
  • 内部类默认是外部类的友元类。
  • 内部类是独立的类,只是作用域被限制在外层类里面;内部类不默认拥有外部类的this指针。
class A { public: class B//B默认就是A的友元 { public: void func(A&a) { cout << a._a << endl; cout << _c << endl; } private: int _c = 3; int _d = 4; }; private: int _a = 1; int _b = 2; }; int main() { A::B b; A a; b.func(a); return 0; }

运行结果:

1 3

9.匿名对象

  • 用类型(实参) 定义出来的对象叫做匿名对象,相比之前我们定义的类型对象名(实参) 定义出来的叫有名对象。
  • 匿名对象生命周期只在当前⼀行,⼀般临时定义⼀个对象当前用一下即可,就可以定义匿名对象。
class A { public: A(int a = 0) :_a(a) { cout << "A(int a)" << endl; } ~A() { cout << "~A()"<<endl; } A(const A& a) { _a = a._a; } private: int _a; }; int main() { //有名对象 A aa1; A aa2(10); //匿名对象--声明周期只在当前一行 A(); A(1); return 0; }

运行结果:

A(int a)//aa1的构造 A(int a)//aa2的构造 A(int a)//A()的构造 ~A() //A()的析构 A(int a)//A(1)的构造 ~A() //A(1)的析构 ~A() //aa2的析构 ~A() //aa1的析构
返回列表