写点什么

C++ 类和对象详解

  • 2022 年 5 月 11 日
  • 本文字数:3416 字

    阅读完需:约 11 分钟

两种创建对象的方式:一种是在栈上创建,形式和定义普通变量类似;另外一种是在堆上使用 new 关键字创建,必须 《一线大厂 Java 面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》无偿开源 威信搜索公众号【编程进阶路】 要用一个指针指向它,读者要记得 delete 掉不再使用的对象。


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


成员变量和函数


=======


类可以看做是一种数据类型,它类似于普通的数据类型,但是又有别于普通的数据类型。类这种数据类型是一个包含成员变量和成员函数的集合。


类的成员变量和普通变量一样,也有数据类型和名称,占用固定长度的内存。但是,在定义类的时候不能对成员变量赋值,因为类只是一种数据类型或者说是一种模板,本身不占用内存空间,而变量的值则需要内存来存储。


类的成员函数也和普通函数一样,都有返回值和参数列表,它与一般函数的区别是:成员函数是一个类的成员,出现在类体中,它的作用范围由类来决定;而普通函数是独立的,作用范围是全局的,或位于某个命名空间内。


class Student{


public:


//成员变量


char *name;


int age;


float score;


//成员函数


void say(){


cout<<name<<"的年龄是"<<age<<",成绩是"<<score<<endl;


}


};


但我们一般建议类内只是声明函数,类外定义。?


class Student{


public:


//成员变量


char *name;


int age;


float score;


//成员函数


void say(); //函数声明


};


//函数定义


void Student::say(){


cout<<name<<"的年龄是"<<age<<",成绩是"<<score<<endl;


}


在类体中直接定义函数时,不需要在函数名前面加上类名,因为函数属于哪一个类是不言而喻的。


但当成员函数定义在类外时,就必须在函数名前面加上类名予以限定。::被称为域解析符(也称作用域运算符或作用域限定符),用来连接类名和函数名,指明当前函数属于哪个类。


在类体中和类体外定义成员函数的区别


=================


在类体中和类体外定义成员函数是有区别的:在类体中定义的成员函数会自动成为内联函数,在类体外定义的不会。当然,在类体内部定义的函数也可以加 inline 关键字,但这是多余的,因为类体内部定义的函数默认就是内联函数。


内联函数一般不是我们所期望的,它会将函数调用处用函数体替代,所以我建议在类体内部对成员函数作声明,而在类体外部进行定义,这是一种良好的编程习惯,实际开发中大家也是这样做的。


当然,如果你的函数比较短小,希望定义为内联函数,那也没有什么不妥的。


如果你既希望将函数定义在类体外部,又希望它是内联函数,那么可以在定义函数时加 inline 关键字。当然你也可以在函数声明处加 inline,不过这样做没有效果,编译器会忽略函数声明处的 inline


构造函数


====


#include <iostream>


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 类中定义了一个构造函数Student(char *, int, float),它的作用是给三个 private 属性的成员变量赋值。要想调用该构造函数,就得在创建对象的同时传递实参,并且实参由( )包围,和普通的函数调用非常类似。


在栈上创建对象时,实参位于对象名后面,例如Student stu("小明", 15, 92.5f);在堆上创建对象时,实参位于类名后面,例如new Student("李华", 16, 96)


构造函数必须是 public 属性的,否则创建对象时无法调用。当然,设置为 private、protected 属性也不会报错,但是没有意义。


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


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

  • 函数体中不能有 return 语句。


构造函数的重载


=======


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


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


默认构造函数


======


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


Student(){}


一个类必须有构造函数,要么用户自己定义,要么编译器自动生成。


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


实际上编译器只有在必要的时候才会生成默认构造函数,而且它的函数体一般不为空。默认构造函数的目的是帮助编译器做初始化工作,而不是帮助程序员。这是 C++的内部实现机制,这里不再深究,初学者可以按照上面说的“一定有一个空函数体的默认构造函数”来理解。


最后需要注意的一点是,调用没有参数的构造函数也可以省略括号。对于示例的代码,在栈上创建对象可以写作Student stu()Student stu,在堆上创建对象可以写作Student *pstu = new Student()Student *pstu = new Student,它们都会调用构造函数 Student()。


以前我们就是这样做的,创建对象时都没有写括号,其实是调用了默认的构造函数。


初始化列表


=====


构造函数的一项重要功能是对成员变量进行初始化,可以在构造函数的函数体中对成员变量一一赋值,还可以采用初始化列表。


#include <iostream>


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){


//TODO:


}


如本例所示,定义构造函数时并没有在函数体中对成员变量一一赋值,其函数体为空(当然也可以有其他语句),而是在函数首部与函数体之间添加了一个冒号:,后面紧跟m_name(name), m_age(age), m_score(score)语句,这个语句的意思相当于函数体内部的m_name = name; m_age = age; m_score = score;语句,也是赋值的意思。


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


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


创建对象时系统会自动调用构造函数进行初始化工作,同样,销毁对象时系统也会自动调用一个函数来进行清理工作,例如释放分配的内存、关闭打开的文件等,这个函数就是析构函数。


析构函数


=======


析构函数(Destructor)也是一种特殊的成员函数,没有返回值,不需要程序员显式调用(程序员也没法显式调用),而是在销毁对象时自动执行。构造函数的名字和类名相同,而析构函数的名字是在类名前面加一个~符号。


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


#include <iostream>


using namespace std;


class VLA{


public:


VLA(int len); //构造函数


~VLA(); //析构函数


public:


void input(); //从控制台输入数组元素


void show(); //显示数组元素


private:


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


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; //释放内存


}


void VLA::input(){


for(int i=0; m_p=at(i); i++){ cin>>*at(i); }


}


void VLA::show(){


for(int i=0; m_p=at(i); i++){


if(i == m_len - 1){ cout<<*at(i)<<endl; }


else{ cout<<*at(i)<<", "; }


}


}


int * VLA::at(int i){


if(!m_arr || i<0 || i>=m_len){ return NULL; }


else{ return m_arr + i; }


}

用户头像

还未添加个人签名 2022.04.13 加入

还未添加个人简介

评论

发布
暂无评论
C++类和对象详解_Java_爱好编程进阶_InfoQ写作社区