C++之类和对象(二)
来源:互联网 发布:哪个军区实力最强 知乎 编辑:程序博客网 时间:2024/06/05 09:26
构造函数和析构函数
1.构造函数
在C++中,有一种特殊的成员函数,它的名字和类名相同,没有返回值,不需要用户显式调用(用户也不能调用),而是在创建对象时自动执行。这种特殊的成员函数就是构造函数(Constructor)。
在 Student类中定义了一个构造函数Student(char *, int, float),它的作用是给三个private属性的成员变量赋值。要想调用该构造函数,就得在创建对象的同时传递实参,并且实参由( )包围,和普通的函数调用非常类似。
在栈上创建对象时,实参位于对象名后面,例如Student stu("小明", 15, 92.5f);在堆上创建对象时,实参位于类名后面,例如new Student("李华", 16, 96)。
构造函数必须是 public属性的,否则创建对象时无法调用。当然,设置为private、protected属性也不会报错,但是没有意义。
构造函数没有返回值,因为没有变量来接收返回值,即使有也毫无用处,这意味着:
不管是声明还是定义,函数名前面都不能出现返回值类型,即使是 void也不允许;
函数体中不能有 return语句。
2.构造函数的重载
和普通成员函数一样,构造函数是允许重载的。一个类可以有多个重载的构造函数,创建对象时根据传递的实参来判断调用哪一个构造函数。
构造函数的调用是强制性的,一旦在类中定义了构造函数,那么创建对象时就一定要调用,不调用是错误的。如果有多个重载的构造函数,那么创建对象时提供的实参必须和其中的一个构造函数匹配;反过来说,创建对象时只有一个构造函数会被调用。
如果用户自己没有定义构造函数,那么编译器会自动生成一个默认的构造函数,只是这个构造函数的函数体是空的,也没有形参,也不执行任何操作。比如上面的 Student类,默认生成的构造函数如下:
Student(){}
一个类必须有构造函数,要么用户自己定义,要么编译器自动生成。一旦用户自己定义了构造函数,不管有几个,也不管形参如何,编译器都不再自动生成。在示例1中,Student类已经有了一个构造函数Student(char *, int, float),也就是我们自己定义的,编译器不会再额外添加构造函数Student(),在示例2中我们才手动添加了该构造函数。
实际上编译器只有在必要的时候才会生成默认构造函数,而且它的函数体一般不为空。默认构造函数的目的是帮助编译器做初始化工作,而不是帮助程序员。这是C++的内部实现机制,这里不再深究,初学者可以按照上面说的“一定有一个空函数体的默认构造函数”来理解。
最后需要注意的一点是,调用没有参数的构造函数也可以省略括号。对于示例2的代码,在栈上创建对象可以写作
Student stu()
或
Student stu,
在堆上创建对象可以写作
Student *pstu = new Student()
或
Student *pstu = new Student,
它们都会调用构造函数 Student()。
3.构造函数的参数初始化表
构造函数的一项重要功能是对成员变量进行初始化,为了达到这个目的,可以在构造函数的函数体中对成员变量一一赋值,还可以采用参数初始化表。
定义构造函数时并没有在函数体中对成员变量一一赋值,其函数体为空(当然也可以有其他语句),而是在函数首部与函数体之间添加了一个冒号:,后面紧跟m_name(name), m_age(age), m_score(score)语句,这个语句的意思相当于函数体内部的m_name = name; m_age = age; m_score = score;语句,也是赋值的意思。
使用参数初始化表并没有效率上的优势,仅仅是书写方便,尤其是成员变量较多时,这种写法非常简明明了
参数初始化表可以用于全部成员变量,也可以只用于部分成员变量。下面的示例只对 m_name使用参数初始化表,其他成员变量还是一一赋值:
Student::Student(char *name, int age, float score): m_name(name){
m_age = age;
m_score = score;
}
#include <iostream>
using namespace std;
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){ } //构造函数对成员变量初始化
void Demo::show(){ cout<<m_a<<", "<<m_b<<endl; }
int main(){
Demo obj(100);
obj.show();
return 0;
}
注意,参数初始化顺序与初始化表列出的变量的顺序无关,它只与成员变量在类中声明的顺序有关在参数初始化表中,我们将 m_b放在了 m_a 的前面,看起来是先给 m_b赋值,再给 m_a赋值,其实不然!成员变量的赋值顺序由它们在类中的声明顺序决定,在 Demo类中,我们先声明的 m_a,再声明的m_b
Demo::Demo(int b): m_b(b), m_a(m_b){
m_a = m_b;
m_b = b;
}
4.初始化const成员变量
参数初始化表还有一个很重要的作用,那就是初始化 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];
}
错误:
class VLA{
private:
const int m_len;
int *m_arr;
public:
VLA(int len);
};
VLA::VLA(int len){
m_len = len;
m_arr = new int[len];
}
5.析构函数
建对象时系统会自动调用构造函数进行初始化工作,同样,销毁对象时系统也会自动调用一个函数来进行清理工作,例如释放分配的内存、关闭打开的文件等,这个函数就是析构函数。
析构函数(Destructor)也是一种特殊的成员函数,没有返回值,不需要程序员显式调用(程序员也没法显式调用),而是在销毁对象时自动执行。构造函数的名字和类名相同,而析构函数的名字是在类名前面加一个~符号。
注意:析构函数没有参数,不能被重载,因此一个类只能有一个析构函数。如果用户没有定义,编译器会自动生成一个默认的析构函数。
我们定义了一个 VLA类来模拟变长数组,它使用一个构造函数为数组分配内存,这些内存在数组被销毁后不会自动释放,所以非常有必要再添加一个析构函数,专门用来释放已经分配的内存。
~VLA()就是 VLA 类的析构函数,它的唯一作用就是在删除对象(第 53行代码)后释放已经分配的内存。
函数名是标识符的一种,原则上标识符的命名中不允许出现~符号,在析构函数的名字中出现的~可以认为是一种特殊情况,目的是为了和构造函数的名字加以对比和区分。
注意:at()函数只在类的内部使用,所以将它声明为 private属性;m_len变量不允许修改,所以用 const限制。
C++ 中的 new和 delete 分别用来分配和释放内存,它们与C语言中malloc()、free()最大的一个不同之处在于:用 new分配内存时会调用构造函数,用 delete释放内存时会调用析构函数。构造函数和析构函数对于类来说是不可或缺的,所以在C++中我们非常鼓励使用new 和delete。
6.构造函数的执行时机
析构函数在对象被销毁时调用,而对象的销毁时机与它所在的内存区域有关。
在所有函数之外创建的对象是全局对象,它和全局变量类似,位于内存分区中的全局数据区,程序在结束执行时会调用这些对象的析构函数。
在函数内部创建的对象是局部对象,它和局部变量类似,位于栈区,函数执行结束时会调用这些对象的析构函数。
new 创建的对象位于堆区,通过delete 删除时才会调用析构函数;如果没有delete,析构函数就不会被执行。
7.this 指针
this 是 C++中的一个关键字,也是一个 const指针,它指向当前对象,通过它可以访问当前对象的所有成员。
void Student::setname(char *name){
this->name = name;
}
void Student::setage(int age){
this->age = age;
}
void Student::setscore(float score){
this->score = score;
}
this 虽然用在类的内部,但是只有在对象被创建以后才会给 this赋值,并且这个赋值的过程是编译器自动完成的,不需要用户干预,用户也不能显式地给 this赋值。
this 是 const指针,它的值是不能被修改的,一切企图修改该指针的操作,如赋值、递增、递减等都是不允许的。
this 只能在成员函数内部使用,用在其他地方没有意义,也是非法的。
只有当对象被创建后 this 才有意义,因此不能在 static 成员函数中使用(后续会讲到static 成员)。
this 实际上是成员函数的一个形参,在调用成员函数时将对象的地址作为实参传递给 this。不过this 这个形参是隐式的,它并不出现在代码中,而是在编译阶段由编译器默默地将它添加到参数列表中。
this 作为隐式形参,本质上是成员函数的局部变量,所以只能用在成员函数的内部,并且只有在通过对象调用成员函数时才给 this赋值。
8.static静态成员变量
对象的内存中包含了成员变量,不同的对象占用不同的内存,这使得不同对象的成员变量相互独立,它们的值不受其他对象的影响。
有时候我们希望在多个对象之间共享数据,对象 a 改变了某份数据后对象 b 可以检测到。
在C++中,我们可以使用静态成员变量来实现多个对象共享数据的目标。静态成员变量是一种特殊的成员变量,它被关键字static修饰
class Student{
public:
Student(char *name, int age, float score);
void show();
public:
static int m_total; //静态成员变量,在BBS数据段
private:
char *m_name;
int m_age;
float m_score;
};
static 成员变量属于类,不属于某个具体的对象,即使创建多个对象,也只为m_total 分配一份内存,所有对象使用的都是这份内存中的数据。当某个对象修改了 m_total,也会影响到其他对象。
static 成员变量必须在类声明的外部初始化,具体形式为:
type class::name = value;
type 是变量的类型,class是类名,name是变量名,value是初始值。将上面的 m_total初始化:
int Student::m_total = 0;
静态成员变量在初始化时不能再加 static,但必须要有数据类型。被private、protected、public修饰的静态成员变量都可以用这种方式初始化。
注意:static成员变量的内存既不是在声明类时分配,也不是在创建对象时分配,而是在(类外)初始化时分配。反过来说,没有在类外初始化的 static成员变量不能使用。
static 成员变量既可以通过对象来访问,也可以通过类来访问。请看下面的例子:
//通过类类访问 static成员变量
Student::m_total = 10;
//通过对象来访问 static成员变量
Student stu("小明", 15, 92.5f);
stu.m_total = 20;
//通过对象指针来访问 static成员变量
Student *pstu = new Student("李华", 16, 96);
pstu -> m_total = 20;
这三种方式是等效的。
注意:static 成员变量不占用对象的内存,而是在所有对象之外开辟内存,即使不创建对象也可以访问。具体来说,static成员变量和普通的 static变量类似,都在内存分区中的全局数据区分配内存。
9.几点说明
1) 一个类中可以有一个或多个静态成员变量,所有的对象都共享这些静态成员变量,都可以引用它。
2) static 成员变量和普通 static变量一样,都在内存分区中的全局数据区分配内存,到程序结束时才释放。这就意味着,static成员变量不随对象的创建而分配内存,也不随对象的销毁而释放内存。而普通成员变量在对象创建时分配内存,在对象销毁时释放内存。
3) 静态成员变量必须初始化,而且只能在类体外进行。例如:
int Student::m_total = 10;
初始化时可以赋初值,也可以不赋值。如果不赋值,那么会被默认初始化为 0。全局数据区的变量都有默认的初始值0,而动态数据区(堆区、栈区)变量的默认值是不确定的,一般认为是垃圾值。
4) 静态成员变量既可以通过对象名访问,也可以通过类名访问,但要遵循 private、protected和 public关键字的访问权限限制。当通过对象名访问时,对于不同的对象,访问的是同一份内存。
10.static静态成员函数
在类中,static 除了可以声明静态成员变量,还可以声明静态成员函数。普通成员函数可以访问所有成员(包括成员变量和成员函数),静态成员函数只能访问静态成员。
编译器在编译一个普通成员函数时,会隐式地增加一个形参 this,并把当前对象的地址赋值给this,所以普通成员函数只能在创建对象后通过对象来调用,因为它需要当前对象的地址。而静态成员函数可以通过类来直接调用,编译器不会为它增加形参this,它不需要当前对象的地址,所以不管有没有创建对象,都可以调用静态成员函数。
普通成员变量占用对象的内存,静态成员函数没有 this指针,不知道指向哪个对象,无法访问对象的成员变量,也就是说静态成员函数不能访问普通成员变量,只能访问静态成员变量。
普通成员函数必须通过对象才能调用,而静态成员函数没有 this 指针,无法在函数体内部访问某个对象,所以不能调用普通成员函数,只能调用静态成员函数。
静态成员函数与普通成员函数的根本区别在于:普通成员函数有 this 指针,可以访问类中的任意成员;而静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)。
和静态成员变量类似,静态成员函数在声明时要加 static,在定义时不能加static。静态成员函数可以通过类来调用(一般都是这样做),也可以通过对象来调用,上例仅仅演示了如何通过类来调用。
11.const关键字
在类中,如果你不希望某些数据被修改,可以使用const关键字加以限定。const可以用来修饰成员变量、成员函数以及对象。
const 成员变量的用法和普通 const变量的用法相似,只需要在声明时加上 const关键字。初始化 const成员变量只有一种方法,就是通过参数初始化表。
const 成员函数可以使用类中的所有成员变量,但是不能修改它们的值,这种措施主要还是为了保护数据而设置的。const成员函数也称为常成员函数。
class Student{
public:
Student(char *name, int age, float score);
void show();
//声明常成员函数
char *getname() const;
int getage() const;
float getscore() const;
private:
char *m_name;
int m_age;
float m_score;
};
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;
}
//定义常成员函数
char * Student::getname() const{
return m_name;
}
int Student::getage() const{
return m_age;
}
float Student::getscore() const{
return m_score;
}
getname()、getage()、getscore()三个函数的功能都很简单,仅仅是为了获取成员变量的值,没有任何修改成员变量的企图,所以我们加了const 限制,这是一种保险的做法,同时也使得语义更加明显。
需要注意的是,必须在成员函数的声明和定义处同时加上 const关键字。char *getname() const和char *getname()是两个不同的函数原型,如果只在一个地方加const 会导致声明和定义处的函数原型冲突。
12.const对象
const 也可以用来修饰对象,称为常对象。一旦将对象定义为常对象之后,就只能调用类的 const 成员了。
定义常对象的语法和定义常量的语法类似:
const class object(params);
class const object(params);
当然你也可以定义 const 指针:
const class *p = new class(params);
class const *p = new class(params);
class为类名,object为对象名,params为实参列表,p为指针名。两种方式定义出来的对象都是常对象。
一旦将对象定义为常对象之后,不管是哪种形式,该对象就只能访问被 const 修饰的成员函数。
但成员变量的访问不限制,但不同通过其进行修改
13.class 和struct的区别
C++ 中保留了C语言的struct 关键字,并且加以扩充。在C语言中,struct只能包含成员变量,不能包含成员函数。而在C++中,struct类似于 class,既可以包含成员变量,又可以包含成员函数。
C++中的 struct和 class基本是通用的,唯有几个细节不同:
使用 class 时,类中的成员默认都是private 属性的;而使用struct 时,结构体中的成员默认都是public 属性的。
class 继承默认是 private继承,而 struct继承默认是 public继承。
class 可以使用模板,而 struct不能。
C++ 没有抛弃C语言中的struct 关键字,其意义就在于给C语言程序开发人员有一个归属感,并且能让C++编译器兼容以前用C语言开发出来的项目。
一个类中可以有 public、protected、private三种属性的成员,通过对象可以访问 public成员,只有本类中的函数可以访问本类的 private成员。现在,我们来介绍一种例外情况——友元(friend)。借助友元(friend),可以使得其他类中的成员函数以及全局范围内的函数访问当前类的private 成员。
fnend 的意思是朋友,或者说是好友,与好友的关系显然要比一般人亲密一些。我们会对好朋友敞开心扉,倾诉自己的秘密,而对一般人会谨言慎行,潜意识里就自我保护。在C++中,这种友好关系可以用friend 关键字指明,中文多译为“友元”,借助友元可以访问与其有好友关系的类中的私有成员。如果你对“友元”这个名词不习惯,可以按原文friend 理解为朋友。
友元函数
在当前类以外定义的、不属于当前类的函数也可以在类中声明,但要在前面加 friend 关键字,这样就构成了友元函数。友元函数可以是不属于任何类的非成员函数,也可以是其他类的成员函数。
友元函数可以访问当前类中的所有成员,包括 public、protected、private属性的。
2) 将其他类的成员函数声明为友元函数
friend 函数不仅可以是全局函数(非成员函数),还可以是另外一个类的成员函数。
- C++之类和对象(二)
- Objective-C之类和对象
- C#OOP之类和对象
- 二、runtime之类和对象(二)
- C++之类和对象(二)
- Objective-C语法之类和对象
- Objective-C语法之类和对象
- 4,Objective-C语法之类和对象
- Objective-C语法之类和对象
- 3、Objective-C语法之类和对象
- Objective-C语法之类和对象
- Objective C之类和对象
- Objective-C语法之类和对象
- Objective-C对象之类对象和元类对象
- Objective-C对象之类对象和元类对象
- Objective-C对象之类对象和元类对象
- java之类和对象
- Scala之类和对象
- 扩增子分析QIIME2. 9训练特征分类集Training feature classifiers with q2-feature-classifier
- GStreamer基础教程01——Hello World
- intent传递字符串数组
- 已知集合A和B的元素分别用不含头结点的单链表存储,函数difference()用于求解集合A与B的差集,并将结果保存在集合A的单链表中
- 【yoyo】类,对象,方法,属性,事件的定义
- C++之类和对象(二)
- 【计算n阶乘后面有多少个0】
- C# 初步学习LINQ
- 方正窝案,立案调查,现已水落石出,等待批判
- 认真学习php面向对象-2
- 玩转正则表达式(Regular),这个世界正在奖励偷偷用心的人
- JAVA中的值传递与引用传递
- 函数与消费
- Matlab投影仿真及三维曲面重构实现及演示程序