自学内容网 自学内容网

类的默认成员函数——构造函数

类的默认成员函数——构造函数

在之前用c++实现栈时用到过一个初始化函数STInit和销毁用函数STDestroy

typedef int DataType;
class Stack {
public:
void STInit() {//初始化栈 
a = NULL;
top = 0;   // top 指向栈顶数据的下一个位置
capacity = 0;//栈中元素数初始化为0 
}

void STDestroy() {//销毁栈 
free(a);//销毁alloc开辟在堆区的内存 
a = NULL;
top = capacity = 0;
}

void STPush(DataType x) {//入栈
checkCapacity();
a[top] = x;
top++;//因为栈顶要指向下一个元素 
}

void STPop() {//出栈
assert(!STEmpty());//栈不应该为空 
top--;//top指向栈顶元素的下一个位置,则退一格即可 
}

DataType STTop() {//返回栈顶元素 
assert(!STEmpty());
return a[top - 1];
//之前top指向栈顶元素的下一个位置 
}

bool STEmpty() {//判断栈是否为空
return top == 0;
}

int STSize() {//返回栈中元素数量 
return top;
}
private:
void checkCapacity() {
if (top == capacity) {
//线性表式扩容,因为栈也是特殊的线性表
int newCapacity = capacity == 0 ? 4 : capacity * 2;
DataType* tmp = (DataType*)realloc(a, newCapacity * sizeof(DataType));
if (tmp == NULL) {
perror("realloc fail");
return;
}
a = tmp;
capacity = newCapacity;
}
}
DataType* a;
int top;//栈顶位置
int capacity;
};

以后类的创建会越来越频繁,这种初始化和销毁的函数能不能自动运行用以简化用户的调用成本?

1 认识默认成员函数

默认的成员函数是不写,编译器会自动生成。

回忆在用c语言写数据结构时,经常忘记初始化变量和释放alloc系列函数申请的空间。祖师爷想着反正都创造了c++了,干脆解决这个问题。

一个类中什么成员都没有,简称为空类。

空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。

默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

c++在构建了类和对象的这个基础上又加了其他的东西,比如默认成员函数,即不写编译器会自动生成的成员函数。

已知的默认成员函数:

  1. 构造函数主要完成初始化工作。
  2. 析构函数主要完成清理工作。
  3. 拷贝构造是使用同类对象初始化创建另一个对象。
  4. 赋值重载主要是把一个对象赋值给另一个对象。
  5. 取地址重载:主要是普通对象和const对象取地址,这两个很少会自己实现。

2 构造函数

构造函数是一个特殊的成员函数,名字与类名相同创建类类型对象时由编译器自动调用,常用于保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次

构造函数虽然名称叫构造,但不是构造对象,而是在构造对象时自动调用这个函数来初始化对象。构造有开空间含义。

#include<iostream>

class A {
public:
A() {
//构造函数用于初始化对象的信息
using std::cout;
cout << "A()";
}
};

int main() {
A a;
return 0;
}

3 构造函数的特征

其特征如下:

  1. 函数名与类名相同。

  2. 无返回值(也不需要加void)。

  3. 对象实例化时编译器自动调用对应的构造函数。

  4. 构造函数可以重载。(可以写很多个自己想要的构造函数来提供多种初始化方式)。

  5. 通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
    例如以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象。

#include<iostream>

class Date {
public:
    // 1.无参构造函数
    Date() {}

    // 2.带参构造函数
    Date(int year, int month, int day) {
        _year = year;
        _month = month;
        _day = day;
    }

    void f() {
        std::cout << "class Date, f()\n";
    }
private:
    int _year;
    int _month;
    int _day;
};

int main() {
    Date d1; // 调无参构造函数
    Date d2(2015, 1, 1); // 调用带参的构造函数
    Date d3();
    //vs编译器发出警告:warning C4930: “Date d3(void)”: 
    //未调用原型函数(是否是有意用变量定义的?)
    //d3.f();//d3的类型被判定为Date(*)(),即被编译器当成了函数
    return 0;
}

构造函数要这么调用:Date d2(2015, 1, 1); ,即在对象后面加括号,里面列出要初始化的数据

首先我们先确立立场,我们是在学习语言,不是在发明语言,遇到任何问题都要按祖师爷的规矩,我们觉得的不重要,除非有一天自己发明语言。

且没有参数不要用Date d3();。因为编译器分不清这个是定义对象还是声明函数。声明函数是后面的括号要写类型。

  1. 如果类中没有显式定义构造函数,则c++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

    比如这个例子。

#include<iostream>

class Date {
public:
/*Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}*/

void Print() {
using std::cout;
cout << _year << "-" << _month << "-" << _day << "\n";
}

private:
int _year;
int _month;
int _day;
};

int main() {
Date d1;
return 0;
}

将Date类中的构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数。

将Date类中构造函数解除屏蔽,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成无参构造函数。

c++把类型分成内置类型(基本类型)和自定义类型。

  • 内置类型就是语言提供的数据类型,如 intchar ⋯ \cdots 以及所有指针(无论是语言提供的数据类型,还是自定义类型的指针)。

  • 自定义类型就是使用classstructunion等自己定义的类型。

编译器生成默认的构造函数会对自定类型成员调用的它的默认构造函数。例如这个例子,Data类自动生成的默认构造函数会调用Time类的构造函数。但是这个自动生成的默认构造函数并不会对其他的成员函数进行处理,这就造成了成员变量的初始值和平常定义局部变量时的初始值一样。

#include<iostream>

class Time {
public:
Time() {
using std::cout;
cout << "Time()\n";
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};

class Date {
public:
int getYear() {
return _year;
}
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};

int main() {
Date d1;
std::cout << std::hex << d1.getYear() << "\n";//vs会输出cccccccc(十六进制),取决于编译器
return 0;
}

一般情况下都需要我们自己写构造函数,决定初始化方式。只有成员变量全是自定义类型的情况可以考虑不写构造函数。但这个不用自己写,自定义类型的成员对象背后的类,那个构造函数要写。

  1. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个

注意:无参构造函数、全缺省构造函数、我们没写构造函数时编译器默认生成的构造函数,这三种函数都可以认为是默认构造函数。

会调用默认构造函数的情况:

  • 声明一个对象。例如Date d1;,即不传参就会调用默认构造函数。

调用默认构造函数时有3种情况:

  1. 啥都没写,编译器自己生成。
  2. 调用无参的构造函数。
  3. 调用全缺省构造函数。

多个默认构造函数并存会存在调用歧义,编译器不知道用哪个。所以无参和全缺省不能同时存在。

默认构造函数对内置类型,如果没有缺省值不会处理,有的话就会。

若没有默认构造函数,而是提供非全缺省构造函数,编译器会报错。

样例:

class Date {
public:
// 1.无参构造函数
Date() {
_year = 1900;
_month = 1;
_day = 1;
}
// 2.带参全缺省构造函数
Date(int year = 1900, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};

int main() {
Date d1;
    return 0;
}

这个类的两个构造函数在语法上没问题,但实际调用时会存在歧义:编译器不知道要调用哪个,于是干脆在编译阶段就报错。此时保留全缺省就行。

构造函数特点的前4点决定了怎么写构造函数及构造函数的基本特性。5、6、7都是在围绕着我们不写的话编译器会生成啥样的构造函数。

4 关于构造函数的后话

构造函数还有以下特点:

  1. 构造函数在公共代码区,这和其他的成员函数一样(详细见类和对象——类的对象占用内存的大小计算)。且构造函数的访问权限是public,否则现在的编译器会阻止编译。

  2. 构造函数有this指针参与

    原因:在声明对象时有这样一个汇编语句:

    lea rcx,[st](不同编译器或不同版本的编译器可能存在差异,这个是我测试时调用的汇编语句),取st的地址放到寄存器rcx中,说明构造函数有办法上传this指针的内容。而且直接在构造函数中调用this指针的行为是允许的。

  3. 在以后的编程中,函数之间调用会越来越频繁且调用层数会越来越深。若函数运行失败,终止程序运行可以用exit(-1); − 1 -1 1可以换成别的,表示程序异常退出。用return只能结束当前函数的运行,可能没法完全阻止程序继续执行后面的任务。

  4. 关于构造函数的特征第6条:自动生成的构造函数,

c++的类型分成两类,内置类型和自定义类型

大部分编译器都不会处理内置类型成员,但有的编译器会处理,这个属于个人行为(即自己的公司想构造函数也处理内置类型成员,就自己写编译器运行c++)。

后来祖师爷后悔这么设计了,有些编译器厂家看祖师爷后悔了,更放开手脚擅自修改底层的汇编代码进行初始化。所以有的小众编译器会自动给内置成员初始化(比如vs2022的哪个版本),无论构造函数是否初始化。

所以为了应对这种行为,后来在c++11标准中支持声明给缺省值。这个操作更像是补构造函数不会处理内置类型的坑。

在这个地方给值并不是初始化,而是声明,即声明给的缺省值。因为在这里写,并不代表占用了内存,要占用内存,还要生成对象。

若用户给的构造函数没给全所有成员函数的初始值,则c++将声明给的缺省值拿过来填。

但即使是c++11,声明给的缺省值只有生成具有物理意义的对象时才生效,通过alloc系列函数申请的对象依旧是随机数(随机数指在不同编译器会产生不同的结果)。

#ifndef _CRT_SECURE_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS 1
#endif

#include<iostream>

//需要编译器支持c++11
class test1 {
public:
int a = 1;
int b = 1;
int* c = (int*)4;//指针也是内置类型 
test1(){
a=2;
}
};

int main() {
using std::cout;
test1 a;
cout << a.a << "\n" <<a.b<<"\n"<<a.c<<"\n";//具有物理意义的对象

test1* c = (test1*)malloc(sizeof(test1));
if (c == NULL)
return -2;
cout << std::hex << c->a << "\n"<< c->c<<"\n";//随机值
c->a=3;//访问权限是public,可在外部修改 
cout<< c->a<<"\n";
free(c);

return 0;
}

程序测试结果之一(g++):
请添加图片描述

  1. c++历史杂谈(心得)

因为各种各样的原因,c语言阶段在语法上有很多弊端。

比如用c语言实现栈时发现栈其实不太好用。

于是在c语言的基础上改进了一下,核心是希望能有更多的功能做到自动运行。但是祖师爷早期的时候没有其他语言的实践经验作为参考,设计时十分复杂。所以c++比起其他面向对象设计语言如java更为复杂。

其次就是一些历史包袱,比如要兼容c语言。也许将这个地方给简化,会简单不少。比如一个类上必须写构造函数,不要什么默认生成。

我们作为学习者,有的东西是因为历史原因,我们没法改变历史,有的东西必须向前兼容,不能做的顺其自然。

就比如构造函数的第6条特征,c++向前兼容,以前出现的坑不敢填,只能造新的坑,让上一个坑显得没那么大。祖师爷的设计理念是内置类型没啥好处理的,于是不处理,自定义类型要处理。

当然,构造函数到这里并不算结束,只是完结了 70 % 70\% 70%~ 80 % 80\% 80%。以后有机会再细谈。


原文地址:https://blog.csdn.net/m0_73693552/article/details/145264034

免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!