初识C++02:类和对象

类和对象

创建方法#

第一种式直接声明:

Copy
class Student { private: string name; public: Student(string name) { this->name = name; } } //声明: Student lu;//不带初始化 //带初始化; Student lu(huang); Student lu = Student(huang); //访问方式 lu.name;

这样的声明方式,是将对象在栈上创建栈内存自动管理,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束后在将这些局部变量的内存空间回收。在栈上分配内存空间效率很高,但是分配的内存容量有限。

第二种使用对象指针声明

Copy
Student *plu = new Student;//不带初始化; Student *plu = new Student(huang);//带初始化; //访问方式 plu->name;

这样声明,是将对象在堆中创建,堆内存代码人员管理,new和delete配对使用。使用 new 在堆上创建出来的对象是匿名的,没法直接使用,必须要用一个指针指向它,再借助指针来访问它的成员变量或成员函数。

函数声明#

成员函数必须先在类体中作原型声明,然后在类外定义,也就是说类体的位置应在函数定义之前。因为类体内定义的函数默认是内联函数,一般用内联函数的时候才会在类内部实现;例子:

Copy
class Student { private: string name; int age; public: //函数声明 Student(string name, int age); } //函数定义 Student::Student(string name, int age) { this->name = name; this->age = age; }

类成员的访问权限以及封装#

在类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制。

在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员。

成员变量大都以m_开头,这是约定成俗的写法,易区分。

建议在开发中不需要暴暴露出来的属性和方法都写成private;

给成员变量赋值的函数通常称为 set XXX函数,读取成员变量的值的函数通常称为 get XXX函数,XXX表示变量名;类中的不写private这些关键词默认是private

Copy
class Student { private: string m_name; public: void setname(string name); void getname; } void Student::setname(string name) { m_name = name;//这里可以直接用m_name; } .....

对象内存模型和函数编译原理及实现#

编译器会将成员变量和成员函数分开存储:分别为每个对象的成员变量分配内存,但是所有对象都共享同一段函数代码,节省空间,sizeof一个对象大小就是全部成员变量的总和大小

C++和C语言的编译方式不同。C语言中的函数在编译时名字不变,或者只是简单的加一个下划线_(不同的编译器有不同的实现),c++是通过一种特殊的算法来实现的,对函数重命名(这个过程叫字编码(Name Mangling));下图是一个编译器重命名的方式,?方法@类名.....

从上图可以看出,成员函数最终被编译成与对象无关的全局函数,如果函数体中没有成员变量,不用对函数做任何处理,直接调用即可。

如果有成员变量(它的作用域不是全局的),C++规定,编译成员函数时要额外添加一个参数,把当前对象的指针传递进去,通过指针来访问成员变量(实际上传递的就是this指针

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

会编译为类似:

Copy
void new_function_name(Demo * const p){//const表示指针不能被修改; //通过指针p来访问a、b cout<<p->a<<endl; }

这样通过传递对象指针就完成了成员函数和成员变量的关联。这与我们从表明上看到的刚好相反,通过对象调用成员函数时,不是通过对象找函数,而是通过函数找对象。

构造函数#

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

一个类必须有构造函数,要么用户自己定义,要么编译器自动生成。一旦用户自己定义了构造函数,不管有几个,也不管形参如何,编译器都不再自动生成

构造函数定义由两种写法:正常函数写法和使用构造函数初始化列表

Copy
//第一种 Student::Student(char *name, int age){ m_name = name; m_age = age; } //第二种 Student::Student(char *name, int age): m_name(name), m_age(age){}

注意👀第二种:成员变量的初始化顺序与初始化列表中列出的变量的顺序无关,它只与成员变量在类中声明的顺序有关;如:

Copy
class Demo{ private: int m_a; int m_b; public: Demo(int b); void show(); }; Demo::Demo(int b): m_b(b), m_a(m_b){ m_a = m_b; m_b = b; } //错误,给 m_a 赋值时,m_b 还未被初始化,它的值是不确定的,所以输出的 m_a 的值是一个奇怪的数字;给 m_a 赋值完成后才给 m_b 赋值,此时 m_b 的值才是 值b。 //obj 在栈上分配内存,成员变量的初始值是不确定的。

使用构造函数初始化列表并没有效率上的优势,但是书写方便,而且,初始化 const 成员变量的唯一方法就是使用初始化列表,原因:为什么要用初始化列表

析构函数#

析构函数(Destructor)也是一种特殊的成员函数,没有返回值,不需要程序员显式调用(程序员也没法显式调用),而是在销毁对象时自动执行。

Copy
class VLA{ public: VLA(int len); //构造函数 ~VLA(); //析构函数 private: const int m_len; //数组长度 int *m_arr; //数组指针 int *m_p; //指向数组第i个元素的指针 }; VLA::VLA(int len): m_len(len){ //使用初始化列表来给 m_len 赋值 if(len > 0){ m_arr = new int[len]; /*分配内存*/ } else{ m_arr = NULL; } } VLA::~VLA(){ delete[] m_arr; //释放内存 }

通过直接用类声明的对象在栈中,出了作用域(比如说函数return了),就会调用析构函数;在全局建的对象在.data区,程序结束后才释放; 注意👀🤷‍♂️:两中方法调用析构函数的顺序都是先生成的后析构,后生成的先析构;

new 创建的对象位于堆区,通过 delete 删除时才会调用析构函数,例如在main函数中new对象然后delete对象;如果声明了变量在堆中,不通过析构函数释放内存,即使外面delete了,也只是删除了指针,里面的空间还是被占用着,没有被释放掉,如上面的int[len];

此处补充指针知识:

Copy
int a = 10; int* p = &a;//p是指针,这个*表示是int的指针类型; 此时 *p = 10//这里的*说明是这个指针指向的对象,与上面*大大不同;而p只是以一个地址

成员对象和封闭类#

一个类的成员变量如果是另一个类的对象,就称之为“成员对象”。包含成员对象的类叫封闭类。

创建封闭类的对象时,它包含的成员对象也需要被创建,这就会引发成员对象构造函数的调用,对于没有默认构造函数的成员对象,必须要使用封闭类构造函数的初始化列表!!!

类名::构造函数名(参数表): 成员变量1(参数表), 成员变量2(参数表), ...
{
//TODO:
}

一定要用初始化列表的四种情况

初始化时:封闭类对象生成时,先执行所有成员对象的构造函数,然后才执行封闭类自己的构造函数;

消亡时:先执行封闭类的析构函数,然后再执行成员对象的析构函数,刚刚好和创建相反。

this指针、static关键字#

C++ 中的一个关键字,也是一个 const 指针, 所以要用->来访问成员变量或成员函数。它指向当前对象,通过它可以访问当前对象的所有成员。

this的本质:this 实际上是成员函数的一个形参,在调用成员函数时将对象的地址作为实参传递给 this。不过 this 这个形参是隐式的,它并不出现在代码中,而是在编译阶段由编译器默默地将它添加到参数列表中。

上述中函数编译原理:成员函数最终被编译成与对象无关的普通函数,除了成员变量,会丢失所有信息,所以编译时要在成员函数中添加一个额外的参数,把当前对象的首地址传入,以此来关联成员函数和成员变量。这个额外的参数,实际上就是 this,它是成员函数和成员变量关联的桥梁。

static修饰成员变量:

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

  • static 成员变量和普通 static 变量一样,都在内存分区中的全局数据区分配内存,到程序结束时才释放。这就意味着,static 成员变量不随对象的创建而分配内存,也不随对象的销毁而释放内存。而普通成员变量在对象创建时分配内存,在对象销毁时释放内存。

  • 静态成员变量必须初始化,而且只能在类体外进行。例如:int Student::m_total = 10;初始化时可以赋初值,也可以不赋值。如果不赋值,那么会被默认初始化为 0。全局数据区的变量都有默认的初始值 0,而动态数据区(堆区、栈区)变量的默认值是不确定的,一般认为是垃圾值。

  • 静态成员变量既可以通过对象名访问,也可以通过类名访问,但要遵循 private、protected 和 public 关键字的访问权限限制。当通过对象名访问时,对于不同的对象,访问的是同一份内存。

static修饰成员函数:

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

原因:编译器在编译一个普通成员函数时,会隐式地增加一个形参 this,并把当前对象的地址赋值给 this,所以普通成员函数只能在创建对象后通过对象来调用,因为它需要当前对象的地址。而静态成员函数可以通过类来直接调用编译器不会为它增加形参 this,它不需要当前对象的地址,所以不管有没有创建对象,都可以调用静态成员函数,所以静态成员函数也无法访问普通成员变量,只能访问静态成员(在全局)。

const 成员变量/成员函数(常成员函数)/对象#

const成员变量:加上 const 关键字。初始化 const 成员变量只有一种方法,就是通过构造函数的初始化列表

const成员函数: const 成员函数可以使用类中的所有成员变量,但是不能修改它们的值,这种措施主要还是为了保护数据而设置的。一般类中的get函数都设置为常成员函数,只读不给改

  • 函数开头的 const 用来修饰函数的返回值,表示返回值是 const 类型,也就是不能被修改,例如const char * getname()

  • 函数头部的结尾加上 const 表示常成员函数,这种函数只能读取成员变量的值,而不能修改成员变量的值,例如char * getname() const

    Copy
    class Student { public: Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score) {}//方法实现都不写; char *getname() const; prviate: char *m_name; int m_age; float m_score; }; char* Student::getname() const { return m_name; }//方法实现都不写;

const对象: const 也可以用来修饰对象,称为常对象。一旦将对象定义为常对象之后,就只能调用类的 const 成员(包括 const 成员变量和 const 成员函数)了,因为非 const 成员可能会修改对象的数据(编译器也会这样假设),C++禁止这样做。

友元函数和友元类#

借助友元(friend),可以使得其他类中的成员函数以及全局范围内的函数访问当前类的 private 成员。在当前类以外定义的、不属于当前类的函数也可以在类中声明,但要在前面加 friend 关键字,这样就构成了友元函数。友元函数可以是不属于任何类的 非成员函数,也可以是其他类的成员函数。

  • 将非成员函数声明为友元函数
Copy
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; }; //非成员函数 void show(Student *pstu){//属于全局函数,通过参数传递对象,可以访问private成员变量 cout<<pstu->m_name<<"的年龄是 "<<pstu->m_age<<",成绩是 "<<pstu->m_score<<endl; }
  • 将其他类的成员函数声明为友元函数,该成员函数提供给一个类用
Copy
class Address; //一定要提前声明Address类 //声明Student类 class Student{ public: Student(char *name, int age, float score); public: void show(Address *addr);//要使用的类,前面有声明address所以不会报错!!!!!!!!!!!!!!!!! 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_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<<"区"<<endl; }

友元类:

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

Copy
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(char *province, 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<<"区"<<endl; } //实现Address类 Address::Address(char *province, char *city, char *district){ m_province = province; m_city = city; m_district = district; }
  • 友元的关系是单向的而不是双向的。如果声明了类 B 是类 A 的友元类,不等于类 A 是类 B 的友元类,类 A 中的成员函数不能访问类 B 中的 private 成员。
  • 友元的关系不能传递。如果类 B 是类 A 的友元类,类 C 是类 B 的友元类,不等于类 C 是类 A 的友元类。
  • 除非有必要,一般不建议把整个类声明为友元类,而只将某些成员函数声明为友元函数,这样更安全一些。

ps: 其实类也是一种作用域 , 普通的成员只能通过对象(可以是对象本身,也可以是对象指针或对象引用)来访问,静态成员既可以通过对象访问,又可以通过类访问,而 typedef 定义的类型只能通过类来访问

struct和class的区别#

C++中,struct 类似于 class,既可以包含成员变量,又可以包含成员函数。

C++中的 struct 和 class 基本是通用的,唯有几个细节不同:

  • 使用 class 时,类中的成员默认都是 private 属性的;而使用 struct 时,结构体中的成员默认都是 public 属性的。
  • class 继承默认是 private 继承,而 struct 继承默认是 public 继承(到继承会讲)。
  • class 可以使用模板,而 struct 不能(到模板会讲)。

建议使用 class 来定义类,而使用 struct 来定义结构体,这样做语义更加明确

posted @   D-booker  阅读(143)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗
点击右上角即可分享
微信分享提示
CONTENTS