-
2021-02-10 05:29:50
在python中,不可能不止一次地定义init函数,因为知道了该语言的工作原理,这是相当公平的。当创建一个对象时,会调用init,因此,拥有其中两个对象将产生不确定性。然而,在某些设计中,这种特性是可取的。例如:class Triangle(object):
def __init__(self,vertices):
self.v1 = vertices[0]
self.v2 = vertices[1]
self.v3 = vertices[2]
def area(self):
# calculate the are using vertices
...
return result
class Triangle(object):
def __init__(self,sides):
self.v1 = sides[0]
self.v2 = sides[1]
self.v3 = sides[2]
def area(self):
# calculate the are using sides
...
return result
在这种情况下,我们有相同数量的属性要初始化,而且它们是相互关联的,因此从一个属性中,您可以获得其他。对,在这个具体的例子中,我们可以处理这样一个事实:顶点是元组,而边可能是浮点数(或字符串或其他东西),但当然情况并非总是如此。在
一种可能的解决方案是将初始化过程委托给其他功能,如:
^{pr2}$
但是它看起来不是很干净,而且所有像“area”这样的功能都必须检查属性是否正确实例化(或者采用try/catch),这是大量的代码,而且会破坏项目的可读性。总的来说,这看起来是一个廉价的伎俩。在
另一个选项是告诉实例要初始化的属性类型:class Triangle(object):
def __init__(self, data, type = "vertices"):
if type == "sides":
data = sideToVertices(self,sides)
else if type == "vertices":
pass
else:
raise(Exception)
self.v1 = data[0]
self.v2 = data[1]
self.v3 = data[3]
def sidesToVertices(self,sides):
# converts sides to vertices
...
return vertices
def area(self):
# calculate the are using vertices
另一种方法似乎更可取,但是我不确定在init中引入逻辑的“pythonic”有多大。你对这件事有什么看法?有没有更好的方法来协调局势?在
更多相关内容 -
C++类的理解(二):函数重载和多个构造函数,以及析构函数
2018-08-14 08:54:35这个函数接受两个整型变量,返回他们的和,但如果我还要一些功能,比如两个double类型的和,一个整型和100的和,并且我也想用add这个函数名怎么办? 函数重载的概念就是用来解决这个问题的,我们...一、函数的重载
函数重载并不属于类的特性,是众多高级语言都有的一种函数特性,比如我有下面的函数://函数1: int add(int a,int b) { return a+b; }
这个函数接受两个整型变量,返回他们的和,但如果我还要一些功能,比如两个double类型的和,一个整型和100的和,并且我也想用add这个函数名怎么办?
函数重载的概念就是用来解决这个问题的,我们把这些函数都写上://函数2: double add(double a,double b) { return a+b; } //函数3: int add(int x) { return x+100; }
以上三个函数就是互为重载函数,他们拥有相同的函数名,但拥有不同的形参表。在代码中调用add函数,系统会根据情况智能的选择最恰当的函数来执行,而选择的标准就是参数表的匹配,举个例子:
int a=10,b=20; double x=10,y=20; cout<<add(a,b); //由于传入参数是两个int,所以系统自动匹配函数1进行调用 cout<<add(x,y); //由于传入的是两个double,所以系统自动匹配函数2进行调用 cout<<add(a); //由于传入的是一个int,与函数三的参数表匹配,所以调用函数3 cout<<add(x); //由于传入的是一个double,没有与之匹配的参数表, //但函数3也是一个参数,系统就会尝试将double类型转换成int类型,然后调用函数3,但我们现在的这个情况是转换不了的,因为double编程int会丢失精度,所以该条语句会报错。
当然如果是下面这种情况却是没有问题的:
char x=10; cout<<add(x); //因为char是8位整型,int是32位整型,系统将char转变成int时不会有问题,所以会返回int型的110;
这就是函数重载,有两个要求:
1、函数名一致
2、参数表不一致(包括参数数量和参数类型)
参数表完全匹配才可以调用,不匹配系统会擅自尝试类型转换以匹配,转换成功还好,不成功就报错。所以写代码时如果需要类型转换的话,最好也由程序员来自己转换,不要太依靠编译器系统。函数重载有两个易犯的错误,其实都很简单,都是因为歧义而出的问题:
第一个:带默认值的函数,比如:int add(int x,int y=1) //y带默认值 { return x+y; } int add(int x) { return x+100; }
所以当有如下语句则报错:
cout<<add(10);
系统会说它匹配了第一个函数(相当于没有给y参数,所以y参数用默认值),也匹配了第二个函数,所以他也不晓得你到底要用哪个函数。。。这个错叫:多个匹配的重载函数。
第二个错也是多个匹配的重载函数:
//函数1: int add(int x,int y) { return x+y; } //函数2: double add(double x, double y) { return x+y }
然后来了个下面的语句:
char x=10,y=20; cout<<add(x,y);
由于传的参数是两个char,没有这样的函数匹配,系统就自己开始类型转换了,他发现可以转成int,然后用函数1,也可以转成double用函数2……于是又晕了,报错:多个匹配的重载函数。
二、类的构造函数(初始化函数)重载
是个函数一般都是可以重载的,类里的函数也不意外,我们来说说构造函数的重载。
举个例子:class Point { public: int x; int y; public: Point() { this->x=0; this->y=0; } Point(int x,int y) { this->x=x; this->y=y; } };
这两个构造函数互为重载函数,一个不带参数,一个带两个参数。
然后有如下对象定义的话:Point t; Point tt(2,5);
第一个t,就是自动调用了没有参数的构造函数,被初始化成:t.x==0, t.y==0;
第二个tt,就自动调用了带两个整型的构造函数,有t.x==2, t.y==5;然后再来看昨天一个问题:
class Circle { public: Point center; int r; public: Circle(int x,int y, int r):center(x,y) { this->r=r; } /* 以及: Circle(int x,int y,int r) { this->center.x=x; this->center.y=y; this->r=r; } */ }
这两种写法是有区别的,第一种没有加注释的写法,是在初始化center的时候,被传了值center(x,y),所以是调用了Point 的有参数的构造函数直接初始化的。
第二种加了注释的写法,是构造center的时候,没有给参数,①:所以调用了Point 没有参数的构造函数,将x,y初始化成了0,②:然后再在Circle构造函数中把center的x,y重新赋了值的。
只有Point自己有不带参的构造函数,注释里的写法才是合法的。构造函数的重载允许我们用各种方法来初始化我们的对象,很是方便的。
三、拷贝构造函数
这是一个特殊的构造函数,用于在一个同类型对象的基础上生成一个新的对象,他的函数形参表有固定的写法,比如以下二叉树节点的类:
class Node { public: int index; Node* left; Node* right; public: Node(int i) //构造函数,带一个参数 { index=i; left=0; right=0; } Node() //构造函数,不带参数 { index=0; left=0; right=0; } Node(const Node& t) //拷贝构造函数的特点是参数表是一个(const className&),t是自定义的变量名 { this->index=t.index; left=0; right=0; } }
拷贝构造函数以自己的一个同类对象为参数,生成一个新的对象,比如:
Node t(10); //调用带一个参数的构造函数 Node x(t); //调用拷贝构造函数 //另外当我们使用赋值号生成新的对象时,系统也是调用拷贝构造函数 Node y=t; //等价于Node y(t);
所以再强调一遍吧,任何用自身同类对象生成新对象时,都会调用拷贝构造函数。
那么有一个问题,我刚刚说用赋值号的时候,系统会调用拷贝构造函数,那我没写拷贝构造函数,我难道不能用赋值号吗?
答:如果没写拷贝构造函数,系统免费送一个默认的。这个默认的构造函数就是简单的把各个属性都对应复制一遍。这种默认的拷贝构造函数,大部分情况下都没有问题,除非:成员变量里有指针!!!
举个例子:class Node { public: int* number public: Node(const Node& t) //默认的拷贝构造函数就这么干的 { this->number=t.number; }
看看它干了啥!!!他把 t 的number里存的地址赋给了新对象的number,这意味着什么? 意味着新对象的
number
和t.number
指着同一个空间,我在新对象里改了number所指空间的值,那么t.number
指的空间值也就变了,这一般不是我们想要的,我们一般想要两个对象互不干扰,所以系统默认给的那种只是简单拷贝了指针地址的这种行为,我们叫浅拷贝,解决方法叫深拷贝,如下:...... Node(const Node& t) { number=new int(); *number=*(t.number); } ......
也就是手动开辟一个新的空间给number,然后把t的number值拷贝进去,这样他们才是互相独立了。
所以当我们的类里存在指针成员变量的时候,又有要用到拷贝构造函数的时候,就要自己动手实现深拷贝。
四、析构函数
析构函数是对象被销毁时,系统最后调用的一个函数,一般用于扫尾工作。析构函数和构造函数一样,没有个话,系统免费送一个,自己定义的话就如下:class Node { public: int index; Node* left; Node* right; public: Node(int i) //构造函数,带一个参数 { index=i; left=0; right=0; } ~Node() //析构函数 { if (left) delete left; if (right) delete right; }
析构函数没有返回值,且不带参数。一般成员变量里有指针时要写,因为这些指针指的空间一般是用new运算符分配的,不会随着对象的销毁而释放,因为系统只会自己释放那个指针用来存地址的空间,它指的那个空间很可能就孤立了,别人也再也指不过去了,所以要手动释放。
以上就是今天的全部内容啦,有不懂的继续问我吧。
-
C++中的类——构造函数
2018-08-29 13:57:31每个类都分别定义了它的对象被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫构造函数。构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造...一、什么是构造函数
每个类都分别定义了它的对象被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫构造函数。构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。
二、构造函数的定义
class 类名
{
访问说明符:
类名(形参列表):初始值列表{函数体定义}
};
————————————————————————————————————————————————
1、构造函数的名字和类名相同、无返回类型有一个(可能为空的)参数列表和一个(可能为空的)函数体。
2、类可以包含多个构造函数,和其他重载函数差不多,不同的构造函数之间必须在参数数量或参数类型上有所区别。
3、不同于其他成员函数,构造函数不能被声明成const的。当我们创建类的一个const对象时,直到构造函数完成初始化过程,对象才能真正取得其“常亮”属性。因此,构造函数在const对象的构造过程中可以向其写值。
4、与其他成员函数相同的是,构造函数在类外定义时也需要明确指出是哪个类。
5、通常情况下,我们将构造函数声明为public的,可以供外部调用。然而有时候我们会将构造函数声明为private或protected的:(1)如果类的作者不希望用户直接构造一个类对象,着只是希望用户构造这个类的子类,那么就可以将构造函数声明为protected,而将该类的子类声明为public。(2)如果将构造函数声明为private,那只有这个类的成员函数才能构造这个类的对象。
默认构造函数
6、默认构造函数:默认构造函数没有参数,即在实例化对象时不需要给定初始值,这时所调用的构造函数为默认构造函数。默认构造函数的初始化规则:(1)如果存在类内初始值,则用它来初始化成员。(2)默认初始化。
7、合成的默认构造函数:当类没有声明任何构造函数时,编译器便会自动地生成默认构造函数。这个由编译器自动创建的构造函数叫做合成的默认构造函数。
8、如果类包含有内置类型或复合类型的成员,则只有当这些成员全部被赋予了类内的初始值时,这个类才适合于使用合成的默认构造函数。因为如果对象定义在块中,则该对象的成员初始值将是不可预测的。
9、有些时候编译器不能为某些类合成默认的构造函数。例如,如果类中包含一个其他类类型的成员且这个成员的类型没有默认构造函数,那么编译器将无法初始化该成员。对于这样的类来说,我们必须自定义默认构造函数,否则该类将没有可用的默认构造函数。当然还有一些其他的情况也会导致编译器无法生成一个正确的默认构造函数。
10、如果类内已经定义了其他的构造函数,那么强烈建议定义一个默认构造函数。我们定义默认构造函数的目的是因为我们即需要在定义类类型的对象时给予初始值的其他形式的构造函数,也需要在定义类类型的对象时不给予初始化参数而根据类内初始值初始化或默认初始化的默认构造函数。我们希望这个函数的作用完全等同于之前使用的合成默认构造函数。
11、在C++11新标准中,如果我们需要默认的行为,那么可以通过在参数列表后写上“=default”(不含引号)来要求编译器生成构造函数。其中,“=default”即可以和声明一起出现在类的内部,也可以作为定义出现在类的外部。在不支持C++11的情况下,可以用一个空参数列表,空初始值列表(见下文)、函数体内不对成员进行赋值操作的构造函数来代替(典型的:类名(){})。
构造函数初始值列表
构造函数初始值列表:类名(形参列表):数据成员1(初始值),数据成员2(初始值)...{函数体}
上面蓝色部分即为构造函数初始值列表,他负责为新创建的对象的一个或几个数据成员赋初始值。
————————————————————————————————————————————————
12、构造函数初始值是成员名字的一个列表,每个名字后紧跟括号括起来的(或者在花括号内)成员初始值。不同成员的初始化通过逗号分隔开来。
13、构造函数初始值列表只说明用于初始化成员的值,而不限定初始化的具体执行循序。实质上,成员的初始化顺序与它们在类定义中的出现顺序一致。然而如果一个成员是用另一个成员初始化的,那么这两个成员的初始化顺序就很重要了。有的编译器会当构造函数的初始值列表中的数据成员顺序与这些成员声明的顺序不符时生成一条警告信息。最好令构造函数初始值的顺序与成员声明的顺序保持一致,并且可能的话,最好用构造函数的参数作为成员的初始值,而避免使用同一个对象的其他成员,这样就不必考虑成员的初始化顺序了。
14、如果没有在构造函数的初始值列表中显式的初始化成员,则该成员将在构造函数体之前执行类内初始值初始化或默认初始化。这一点也就解释了在不支持C++11的情况下如何生成一个执行默认操作的默认构造函数(见第11点)。
15、一旦构造函数体开始执行,也就意味着初始化结束了。可以在构造函数体内部重新为成员赋值。有时我们可以忽略数据成员初始化和赋值之间的差异,但并非总能这样。例如,如果成员是const或者引用或属于某种类类型且该类没有定义默认构造函数时,必须将这个成员初始化。
默认实参构造函数
16、构造函数也可以具有默认的实参,如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。
三、委托构造函数
class 类名
{
public:
类名(int a,string b,vector<string> c):aa(a),bb(b){ cc=c; }//构造函数
类名(形参列表):类名(0," "){}//委托构造函数
类名(形参列表):类名(5,"hellol"){}//委托构造函数
private:
int aa;
string bb;
vector<string> cc;
};
————————————————————————————————————————————————
17、C++11新标准扩展了构造函数初始值的功能,我们可以定义委托构造函数。一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它自己的一些(或者全部)职责委托给了其他构造函数。
18、委托构造函数也有一个成员初始值列表(相当于对构造函数的调用)和一个函数体。在委托构造函数内,成员初始值列表只有一个唯一的入口,就是类名本身。和其他成员初始值一样,类名后面紧跟圆括号括起来的参数列表,参数列表必须与另一个构造函数匹配。
19、当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行,然后控制权才会交给委托者的函数体。
20、委托可以多级传递,即A可以委托给B,B又可以委托给C。
四、隐式的类类型转换
21、如果构造函数只接受一个实参,则它便具有了转换为此类类型的隐式转换机制。把这种构造函数称为转换构造函数。具体表现在:在成员函数的参数为类类型时,可以为其传一个数据成员值,该值会自动转换成一个类类型的临时对象作为实参。
22、需要注意以下几点;(1)转换前传递的实参必须具有对应的转换构造函数。(2)编译器只会自动的执行一步类型转换。(例如,需要一个string类型作为形参的转换构造函数,我们不能用一个字符串字面量作为实参去掉用需要该类类型作为形参的成员函数,因为这时需要两部转换)(3)临时的中间变量都是const,所有没有const的引用会失败。所以如果成员函数的形参为该类类型的引用,则必须将其定义为const类型。
#include <iostream> using namespace std; class part { public: part(string s):ss(s),ii(10){} part(int i,string s):ii(i),ss(s){} void foo(const part& partn){ss=partn.ss;} void print() { cout<<ii<<endl<<ss<<endl; } private: int ii; string ss; }part1(100,"world"); int main() { part1.print(); string s="hello"; part1.foo(s); part1.print(); }
23、可以用explicit修饰构造函数从而达到如下效果(explicit只允许出现在类的内部):
23.1、如果构造函数是转换构造函数,此时将无法进行隐式转换。
23.2、发生隐式转换的另一种情况是,当我们执行拷贝形式的初始化时(=)。此时,只能使用直接初始化,而不能将explicit构造函数用于拷贝形式的初始化。
24、尽管编译器不会将explicit的构造函数用于隐式转换过程,但是可以显式的强制进行转换。
part1.foo(part("hellio"));
part1.foo(static_cast<part>"hello");
25、标准库中的构造函数
25.1、接受一个单参数的const char*的string构造函数不是explicit的。
string s="hello";
string s("hello");
25.2、接收一个容量参数的vector构造函数是explicit的。
vector<T> v(n);
五、聚合类
26、聚合类与C语言中的结构体很类似,它使得用户可以直接访问其成员,并且具有特殊的初始化语法形式。
27、当一个类满足如下条件时,我们说它是聚合类:
27.1、所有成员都是public的
27.2、没有定义任何构造函数
27.3、没有类内初始值
27.4、没有基类,也没有virtual函数。
28、可以用一个花括号括起来的成员初始值列表初始化聚合类的数据成员,初始值的顺序必须与声明的顺序一致。
29、如果初始值列表中的元素个数少于类的成员数量,则靠后的成员被值初始化。初始值列表的元素个数绝对不能超过类的成员数量。
六、字面值常量类
此部分可参考——C++11中的constexpr
30、数据成员都是字面值类型的聚合类是字面值常量类。
31、如果一个类不是聚合列,但它符合下述要求,则它也是一个字面值常量类。
31.1、数据成员都必须是字面值类型
31.2、类必须至少含有一个constexpr构造函数(尽管构造函数不能是const的)
31.3、如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数
31.4、类必须使用析构函数的默认定义,该成员负责销毁类的对象。
32、constexpr构造函数可以声明成=default的形式(或者是删除函数的形式)。否则,constexpr构造函数就必须既符合构造函数的要求(不能包含返回语句),又符合constexpr函数的要求(意味着它能拥有的唯一可执行语句就是返回语句)。综合这两点,constexpr构造函数体应该是空的。通过前置关键字constexpr就可以声明一个constexpr构造函数了。
33、constexpr构造函数必须初始化所以数据成员,初始值要么使用constexpr构造函数,要么是一条常量表达式。
34、constexpr构造函数用于生成constexpr对象以及constexpr函数的参数或返回类型。
#include <iostream> using namespace std; #include <string> class part { public: constexpr part(int i, int s) :ii(i), ss(s) {} void print() { cout << ii << endl << ss << endl; } constexpr int poo() { return ss; } private: int ii; int ss; //只能使用字面值类型 }; int main() { constexpr part p1(10, 20); int data[p1.poo()]; cout << sizeof(data) << endl; }
-
【C++】C++类和对象、构造函数和析构函数
2018-06-03 20:32:37类 类是对某一事物的抽象描述,具体地讲,类是C++中的一种构造的数据类型。它即可包含描述事物的数据,又可包含处理这些...定义一个类的一般格式为: class 类名{ private: 成员表1; public: 成员表2; protected:...类
类是对某一事物的抽象描述,具体地讲,类是C++中的一种构造的数据类型。它即可包含描述事物的数据,又可包含处理这些数据的函数,类在程序运行时是被用作样板来建立对象的。所以要建立对象,首先必须定义类。
定义类
定义一个类的一般格式为:
class 类名{ private: 成员表1; public: 成员表2; protected: 成员表3; };
其中,成员表可以是数据说明或者是函数说明,这与结构体类型的说明是一样的。
class Person { private: char Name[12]; int Age; char Sex[4]; public: void RegisterPerson(const char*, int, const char*); void GetName(char*); int GetAge(void); void GetSex(char *); };
关键字private、public、protected的作用是限定成员的访问权限,这三个关键字在类中的使用先后顺序无关紧要,并且每一个关键字在类体中可使用多次。同样的,类体中成员和成员函数的定义顺序,也无关紧要。
- 关键字private限定的成员称为私有成员,对私有成员限定在该类的内部使用,即只允许该类中的成员函数存取私有的成员数据;对私有成员函数,只能被该类中的成员函数调用;
- 关键字public限定的成员称为公有成员,这种成员不仅允许该类中的成员函数存取公有成员数据,还允许该类之外的函数存取公有成员数据;公有成员函数不仅能被该类的成员函数调用,而且还允许该类之外的函数调用;
- 关键字protected限定的成员称为保护成员,它允许该类的成员函数存取保护成员数据,可调用保护成员函数,也允许该类的派生类的成员函数存取保护成员数据或者调用保护成员函数。但其他函数不能存取该类的保护成员数据,也不能调用保护成员函数。
在类体中,当省略关键字private时,系统默认为所定义的成员数据为私有成员,即在类体中没有明确地指出成员的访问权限时,系统约定这些成员为私有成员。
在类中,仅仅给出了成员函数的函数原型,并没有给出成员函数的函数定义。在使用这些成员函数前,必须先给出这些函数的定义。定义一个类的成员函数的一般格式为:
数据类型 类名::函数名(参数){ ... }
其中,“::”称为作用域运算符,它指出该函数是某一个类的成员函数。
void Person::RegisterPerson(const char* name, int age, const char* sex) { strcpy(Name, name); Age = age; strcpy(Sex, sex); } void Person::GetName(char* name) { strcpy(name, Name); } int Person::GetAge(void) { return Age; } void Person::GetSex(char* sex) { strcpy(sex, Sex); }
这边先总结一下“::”的使用:
- 全局作用域符,在局部变量和全局变量冲突的时候,使用全局变量;
- 结构体作用域符,结构体中的静态成员,必须在文件作用域中的某个位置对静态的成员进行定义性说明;
数据类型 结构体类型名::静态成员名;
- 类作用域符,类成员函数的具体定义;
- 命名空间作用域符,比如想调用namespace std中的cout成员,就可以写成std::cout;
- 在派生类由多个基类派生,发生同名成员冲突时,使用作用域运算符来限定成员属于哪一个基类。
在定义一个类的时候,需要注意的点:
- 类具有封装性,并且类只是定义了一种结构,所以在类中的任何成员数据均不能使用auto、extern、register限定其存储类型;
- 成员函数可以直接使用本类中的任一成员,包括数据成员和函数成员;
- 在定义类时,只是定义了一种构造数据类型,并不为类分配存储空间,所以在定义类中的数据成员时,不能对其初始化。
类和结构体类型
从类的定义格式可以看出,类与结构体类型时类同的,类的成员可以是数据成员或函数成员,结构体中的成员与此类似,并且在结构体中,也可以使用关键字private、public和protected限定其成员的访问权限。
实际上,在C++中,结构体类型只是类的一个特例。结构体类型和类的唯一区别在于:在类中,其成员的缺省的存取权限是私有的;而在结构体类型中,其成员的缺省的存取权限是公有的。
注意一下:在C语言中,结构体中是不能定义函数的,但是可以通过函数指针来实现函数的功能。C++中的struct对C中的struct进行了扩充,它已经不再只是一个包含不同数据类型的数据结构了,它已经获取了太多的功能。struct能包含成员函数、能限定成员的访问权限、能继承、能实现多态。
当然,struct更适合看成是一个数据结构的实现体,class更适合看成是一个对象的实现体。
内联成员函数
当定义一个类时,其成员函数的函数体的定义也可以在类的类体中直接定义,即在类中直接定义成员函数。
class Person { private: char Name[12]; int Age; char Sex[4]; public: void RegisterPerson(const char* name, int age, const char* sex) { strcpy(Name, name); Age = age; strcpy(Sex, sex); } void GetName(char* name) { strcpy(name, Name); } int GetAge(void) { return Age; } void GetSex(char* sex) { strcpy(sex, Sex); } };
类中定义成员函数和用“::”类外定义成员函数,使用效果相同。两者的区别在于:
- 在定义类时,在类体中直接定义成员函数的函数体时,这种成员函数在编译时是作为内联函数来实现的,所以也称为内联成员函数;
- 用“::”类外定义成员函数则不作为内联函数来实现。
通常当函数的功能比较简单时,定义为内联成员函数;而当函数的实现比较复杂,则不使用内联成员函数。
定义内联成员函数的另一种方法是:在类体中同样只给出成员函数的原型说明,在类体外定义成员函数时,与定义一般的内联函数一样,在成员函数的定义前面加上关键字inline。
对象
对象的说明
对象也称作是类的实例,和结构体变量一样,对象必须先定义后使用。说明对象的一般格式为:
类名 对象名; Person p1, p2;
系统并不为类分配存储空间,当说明对象时,系统才为对象分配相应的存储空间。为对象分配存储空间的大小,取决于定义类时所定义的成员的类型和成员的多少。
不同的对象占据内存的不同区域,它们所保存的数据各不相同,但对成员数据进行操作的成员函数的程序代码均是一样的。也就是说,p1.Name和p2.Name的值可能是不同的,但是p1.GetName()和p2.GetName()的代码是相同的。
为了减少成员函数所占用的空间,在建立对象时,只为对象分配保存成员数据的内存空间,而成员函数的代码为该类的每一个对象所共享。通常,类中定义的成员函数的代码被存放在计算机内存的一个公共区内,并供该类的所有对象共享。这是C++实现对象的一种方法。只是逻辑上,我们一般仍将每一个对象理解成由独立的成员数据和各自的成员函数代码组成。
说明对象的方法与说明结构体变量的方法一样,也有三种:
- 先定义类的类型,再说明对象;
- 在定义类的同时说明对象;
- 直接说明对象,而不定义类的类名。
在定义对象时,当类中的数据成员的访问权限指定为公有时,定义对象时允许对它的成员数据进行初始化;当类中的数据成员的访问权限指定为私有或者保护时,则定义对象时不允许对它的成员数据进行初始化。
对象的使用
- 对象通过选择运算符“.”来访问对象的成员。当访问一个成员函数时,也称为向对象发送一个消息。用成员选择运算符“.”只能访问对象的公有成员,而不能访问对象的私有成员或者保护成员。若要访问对象的私有成员或者保护成员,只能通过对象的公有成员函数来获取;
- 同类型的对象之间可以整体赋值;
- 对象作为函数的参数时,属于赋值调用;函数可以返回一个对象;
- 一个类的对象可以作为另一个类的成员。
类的嵌套
在定义一个类时,在其类体中又包含了一个类的定义,称为类的嵌套。比如:
class Outer{ public: class Inner{ public: int x,y; }; Inner c1,c2; float a,b; };
可以将嵌套类看作是一种成员类,系统并不为嵌套类分配内存空间,说明该嵌套类的成员时,也不为其分配内存空间。仅仅是定义外层类的对象时,才会为嵌套类的成员分配内存空间,但此时嵌套类还是没有内存空间。嵌套类的类名作用域从其定义开始,到其外层类的定义结束时结束。
成员函数的重载
类中的成员函数与普通函数一样,成员函数可以带有缺省参数,也可以重载成员函数。
例子(线性表):
class ListClass { private: int* List; unsigned nMax; //线性表最大长度 unsigned nElem; //表中的数据个数 public: void Init(int n = 10) { //初始化线性表 List = new int[n]; nMax = n; nElem = 0; } int Elem(int n) { //返回第n个元素的值 if ((n >= 0) && (n < nElem)) return List[n]; else return 0; } int &Elem(unsigned n) { //返回第n个元素的引用 return List[n]; } unsigned Elem(void) { //返回线性表长度 return nElem; } };
this指针
用对象的成员函数来访问对象的成员时,在成员函数的实现中,只要给出成员名就可实现对该对象成员的访问;但在成员函数之外要访问某一个成员时,须指明访问的是哪一个对象的成员。
实际上,当调用一个成员函数时,系统会自动地向它传递一个隐含的参数,该参数是一个指向接受该函数调用的对象的指针,在成员函数的函数体中可以直接使用关键字this来访问这个指针。在成员函数的实现中,当访问该对象的某一个成员时,系统自动地使用了该隐含的this指针。比如:
int Elem(int n) { //返回第n个元素的值 if ((n >= 0) && (n < this->nElem)) return this->List[n]; else return 0; }
this指针很具有如下形式的缺省说明:
类名* const this; ListClass* const this;
即把该指针说明为const指针,只允许在成员函数体中使用该指针,但不允许改变该指针的值。
一般而言,我们无需关心该指针,它是由系统自动维护的。但在某些特殊的应用场合下,可能会用到该指针。比如,线性表中需要一个拷贝线性表的函数:
void CopyList(ListClass L) { nMax = L.nMax; nElem = L.nElem; if (List) delete []List; //A List = new int[nMax]; for (int i = 0; i < nElem; i++) List[i] = L.Elem(i); }
一看没有什么问题,但是一旦出现某线性表自己拷贝自己的情况,执行到A行的时候,就销毁线性表的内存空间,再new运算符为线性表动态分配存储空间。也就是说,线性表中的数据全部丢失了,所以数据的拷贝肯定无法实现。为了防止这种情况:
void CopyList(ListClass L) { if (&L != this) { nMax = L.nMax; nElem = L.nElem; if (List) delete[]List; List = new int[nMax]; for (int i = 0; i < nElem; i++) List[i] = L.Elem(i); } }
构造函数
在产生对象时,对对象的数据成员进行初始化的方法有三种:第一种是使用初始化数据列表的方法,第二种是通过构造函数实现初始化,第三种是通过对象的拷贝初始化函数来实现。
第一种只能对类的公有数据成员初始化,而不能对私有的或保护的数据成员进行初始化。通常使用构造函数来实现对对象的数据成员的初始化较为常见。构造函数是类的成员函数,系统约定构造函数名必须与类名相同。
定义构造函数
在定义一个类时,可根据需要定义一个或多个构造函数(重载构造函数)。构造函数与类的成员函数一样,可以在类中定义结构体,也可在类外定义函数体。在类中定义构造函数的一般格式为:
类名(参数列表){ ... }
在类外定义构造函数的一般格式为:
类名::类名(参数列表){ ... }
对构造函数,须说明以下几点:
- 构造函数的函数名必须与类名相同。构造函数的主要作用:是完成初始化对象的成员数据以及其他初始化工作;
- 因为构造函数是由系统自动调用的,构造函数与其他成员函数不一样,在定义构造函数时,不能指定函数返回值的类型,也不能指定为void类型;
- 构造函数可以不带参数,也可以带若干个参数,也可以指定参数的缺省值。在定义多个构造函数时,必须满足函数重载的原则,即所带的参数个数或参数的类型是不同的;
- 若定义的类要说明该类的对象时,构造函数必须是公有的成员函数。如果定义的类仅用于派生其他类时,则可将构造函数定义为保护的成员函数。
构造函数和对象的初始化
当定义了类的构造函数后,在产生该类的一个对象时,系统根据定义对象时给出的参数自动调用对应的构造函数,完成对象的成员数据的初始化工作。由于构造函数属于类的成员函数,它对私有成员数据、保护的成员数据和公有的成员数据均能进行初始化。比如:
#include <iostream> using namespace std; class Rectangle { private: int Left, Right, Top, Bottom; public: Rectangle(int L, int R, int T, int B) { Left = L; Right = R; Top = T; Bottom = B; } Rectangle() { Left = 0; Right = 0; Top = 0; Bottom = 0; } }; int main() { Rectangle r1(1, 2, 3, 4); //A:调用带参数的构造函数 Rectangle r2; //B:调用不带参数的构造函数 Rectangle r3(); //C:一个不带参数,返回值为Rectangle类型的函数声明 system("pause"); return 0; }
需要注意一下:
- 如果需要调用不带参数的构造函数(B行),定义类的对象时,后面不需要给出一对括号;
- 如果在定义对象的时候,后面加上一对括号,这就不表示定义对象了,更不表示调用不带参数的构造函数。表示的是不带参数,返回值为类对象类型的声明。
只有遵循这一种约定之后,系统才能够区分是对不带参数的函数的原型说明,还是定义对象。
全局对象、静态对象、局部对象的定义:
- 定义全局对象,构造函数在main()函数执行之前就被调用;
- 定义静态对象,仅会调用一次构造函数;
- 定义局部对象,每次声明都会调用一次。
注意:这里说的是定义的时候,只针对这种情况的语句。
构造函数与new运算符
可以使用new运算符来动态地建立对象。用new运算符建立对象时,同样的也要自动调用构造函数,以便完成对象的成员数据初始化。比如:
Rectangle* r1 = new Rectangle(1, 2, 3, 4);
当使用new运算符建立一个动态的对象时,new运算符首先为类Rectangle的对象分配一个内存空间,然后自动地调用构造函数来初始化对象的成员数据,最后返回该动态对象的起始地址。
注意,用new运算符产生的动态对象,在不再使用这种对象时,必须用delete运算符来释放对象所占用的存储空间。即:
delete r1;
也就是说,使用new运算符动态创建的对象,最终返回的是一个指针类型的数据;而不使用new运算符,就直接创建一个对象。而且指针类型用“->”来引用对象的成员,而类的对象用“.”来引用对象的成员。除此之外,new运算符创建出来的对象,要及时delete释放掉。
缺省的构造函数
在定义类时,若没有定义类的构造函数,则编译器自动产生一个缺省的构造函数,其格式为:
类名::类名(){ }
从定义格式上来看,这是一个函数体为空的构造函数,即在产生对象时,尽管也调用缺省的构造函数,但函数什么事也不做。所以缺省的构造函数并不对产生对象的数据成员赋初值,也就是说,尽管产生对象时调用了缺省的构造函数,但新产生的对象的数据成员的值依然是不确定的。
需要注意的是:在定义类时,一旦定义了类的构造函数,编译器就不会产生缺省的构造函数。并且缺省的构造函数只能有一个。缺省的构造函数包括两种情况:没有参数的构造函数或者各参数均有缺省值。
什么意思呢?也就是说,如果在定义了一个类的时候,同时也定义了非缺省的构造函数,那么就一定要手动补上缺省的构造函数,除非不要使用非缺省的构造函数来创建对象。
例如:
#include <iostream> using namespace std; class Rectangle { private: int Left, Right, Top, Bottom; public: Rectangle(int L, int R, int T, int B) { Left = L; Right = R; Top = T; Bottom = B; } }; int main() { Rectangle r1; //出错:定义了类的构造函数,编译器就不产生缺省的构造函数 system("pause"); return 0; }
#include <iostream> using namespace std; class Rectangle { private: int Left, Right, Top, Bottom; public: Rectangle(int L=0, int R=0, int T=0, int B=0) { Left = L; Right = R; Top = T; Bottom = B; } Rectangle() { Left = 0; Right = 0; Top = 0; Bottom = 0; } }; int main() { Rectangle r1; //出错:在编译时出现两个缺省的构造函数,产生二义性 system("pause"); return 0; }
析构函数
产生对象时,系统要为对象分配存储空间;在对象结束其生命周期或结束其作用域(静态存储类型的对象除外)时,系统要回收对象所占用的存储空间,即要撤销一个对象。这个工作是由析构函数来完成的。
定义析构函数
析构函数也是类的成员函数,定义析构函数的格式为:
类名::~类名(){ ... }
关于析构函数,须说明以下几点:
- 析构函数名必须与类名相同,并在其前面加上字符“~”,以便和构造函数名相区分;
- 析构函数不能带有任何参数,不能有返回值。换言之,析构函数是唯一的,析构函数不允许重载;
- 析构函数是在撤销对象时由系统自动调用的,它的作用是在撤销对象前做好结束工作。在析构函数内要终止程序的执行时,不能使用库函数exit(),但可以使用函数abort()。因为exit()函数要做程序前的结束工作,它又要调用构造函数,形成无休止的递归;而abort函数不做终止前的结束工作,直接终止程序的执行;
- 在程序的执行过程中,当遇到某一个对象的生存周期结束时,系统自动调用析构函数,然后再回收为该对象所分配的存储空间。
另外:如果在构造函数中,使用了new运算符为对象的某个指针成员分配了动态申请的内存空间,那么必须定义一个析构函数,来使用delete运算符删除new出来的成员的内存空间。
同时:当使用delete运算符删除由new运算符动态生成的对象时,它首先调用该对象的析构函数,然后再释放该对象所占用的内存空间。这与new运算符创建动态对象的过程正好相反。这是因为:系统不能自动调用析构函数来撤销动态生成的对象。
上面两句话,可能会比较绕,它们之间的核心区别是:一个是类中的某个成员是new出来的,那么就需要在析构函数中将这个成员delete掉;一个是类的对象是new出来的,那么就在delete这个对象时调用析构函数。
比如前者:
class String { private: char* Buffer; public: String(char* s) { if (s) { Buffer = (char*)new char[strlen(s) + 1]; strcpy_s(Buffer, strlen(s) + 1, s); } else Buffer = 0; } ~String() { if (Buffer) { delete[]Buffer; } } };
比如后者:
#include <iostream> using namespace std; class Rectangle { private: int Left, Right, Top, Bottom; public: Rectangle(int L=0, int R=0, int T=0, int B=0) { Left = L; Right = R; Top = T; Bottom = B; } ~Rectangle() { cout << "调用析构函数!" << endl; } void point() { cout << Left << '\t' << Right << '\t' << Top << '\t' << Bottom << endl; } }; int main() { Rectangle* p = new Rectangle(1, 2, 3, 4); p->point(); delete p; cout << "退出主函数!" << endl; system("pause"); return 0; }
这段程序的运行结构为:
1 2 3 4 调用析构函数! 退出主函数! 请按任意键继续. . .
用delete运算符撤销动态生成的对象数组
用delete运算符撤销单个对象与撤销对象数组时,其用法有所不同。比如:
#include <iostream> using namespace std; class Rectangle { private: int Left, Right, Top, Bottom; public: Rectangle(int L = 0, int R = 0, int T = 0, int B = 0) { Left = L; Right = R; Top = T; Bottom = B; cout << "调用构造函数!" << endl; } ~Rectangle() { cout << "调用析构函数!" << endl; } }; int main() { Rectangle* p = new Rectangle[2]; delete []p; cout << "退出主函数!" << endl; system("pause"); return 0; }
这段程序的运行结构为:
调用构造函数! 调用构造函数! 调用析构函数! 调用析构函数! 退出主函数! 请按任意键继续. . .
首先,使用new运算符来动态地建立对象数组,会依次自动调用构造函数。但是在使用delete运算符来释放指向的对象数组所占用的存储空间时,在指针变量的前面必须加上[]。如果不加,仅仅释放对象数组的第0个元素,即只调用数组的第0个元素的析构函数,其他元素的占用空间不释放。但在VC++环境下,将产生运行错误。
缺省的析构函数
与缺省的构造函数一样,若在类中没有显式地定义析构函数,则编译器自动地产生一个缺省的析构函数,其格式为:
类名::~类名(){ }
缺省的析构函数的函数体为空,即该缺省的析构函数什么也不执行。实际上,任何对象都有构造函数和析构函数。
在产生对象时,若不对数据成员进行初始化,可以不显式地定义构造函数;当撤销对象时,若不做任何结束工作,可以不显式地定义析构函数。但在撤销对象时,如果要释放的对象拥有用new运算符分配内存空间的成员,则必须显式地定义析构函数。
不同存储类型的对象调用构造函数和析构函数
通常在产生对象的时候调用构造函数,在撤销对象的时候调用析构函数。但对于不同存储类型的对象,调用构造函数与析构函数的情况有所不同。
- 对于全局定义的对象,在程序开始执行时,调用构造函数,到程序结束时,调用析构函数;
- 对于局部定义的对象,当程序执行到定义对象时,调用构造函数,在退出对象作用域时,调用析构函数;
- 对于static定义的局部变量,在首次到达对象的定义时,调用构造函数,在程序结束时,调用析构函数;
- 对于用new运算符动态生成的对象,在产生对象时调用构造函数,只有使用delete运算符来释放对象时,才调用析构函数。也就是说,系统不能自动调用析构函数来撤销动态生成的对象。
实现类型转换与拷贝的构造函数
实现类型转换的构造函数
下面通过举例来说明何时需要显式地实现类型转换的构造函数,何时使用隐式地实现类型转换的构造函数。
#include <iostream> using namespace std; class Ex1 { private: int x; public: Ex1(int a) { x = a; cout << x << " 调用构造函数!" << endl; } ~Ex1() { cout << x << " 调用析构函数!" << endl; } }; int main() { Ex1 x1(50); //A Ex1 x2 = 100; //B x2 = 200; //C cout << "退出主函数!" << endl; system("pause"); return 0; }
这段程序的运行结果是:
50 调用构造函数! 100 调用构造函数! 200 调用构造函数! 200 调用析构函数! 退出主函数! 200 调用析构函数! 50 调用析构函数! 请按任意键继续. . .
首先先补充一点:当构造函数只有一个数值参数时,下面两句是一样的意思:
Ex1 x2 = 100; Ex1 x2(100);
注意:在这种情况下的等号是传递单个数值到构造函数的另一种方法,这是初始化对象而不是赋值!
上面这段程序只定义了两个对象,却出现了调用三次构造函数,三次析构函数的情况。解释一下:
运行A、B两句的时候,进行对象的初始化,调用构造函数,没有什么问题。到了C句,这是一个赋值语句,不是初始化语句。此时应该这样理解:x2应该接受一个Ex1类型的对象(只有同类型的对象之间才可以赋值)。这是编译器会调用构造函数将200转换成Ex1类型的对象,即产生一个临时的对象,并将该对象赋给x2。为此,第三次调用构造函数。一旦完成这种赋值,立即撤销该临时对象,即调用析构函数。接下来的程序就比较简单了。
那么什么是显式地实现类型转换的构造函数,什么是隐式地实现类型转换的构造函数?比如:
x2 = 200; //隐式 x2 = Ex1(200); //显示
又该何时使用何种:只有当构造函数只有一个参数时,才能隐式地实现类型转换的构造函数;当构造函数有多个参数时,必须显式地实现类型转换的构造函数。
用构造函数进行类型转换的一般格式:
对象名 = 类名(构造函数的参数列表);
作用是:首先产生一个临时的对象,完成赋值后,立即撤销该临时的对象。
要注意和生成对象的区别:
类名 对象名(构造函数的参数列表);
完成拷贝功能的构造函数
完成拷贝功能的构造函数的一般格式为:
类名::类名(类名 &形参){ ... }
实现拷贝功能的构造函数的参数是该类类型的引用。显然,用这种构造函数来创建一个对象时,必须用一个已产生的同类型对象作为实参。
例如:
#include <iostream> using namespace std; class Test { private: int x, y; public: Test(int a, int b) { x = a; y = b; cout << "调用构造函数!" << endl; } Test(Test &t) { x = t.x; y = t.y; cout << "调用完成拷贝的函数!" << endl; } }; int main() { Test t1(50,100); Test t2 = t1; //A Test t3(t1); //B cout << "退出主函数!" << endl; system("pause"); return 0; }
这段程序的运行结果为:
调用构造函数! 调用完成拷贝的函数! 调用完成拷贝的函数! 退出主函数! 请按任意键继续. . .
执行到A行和B行时,自动将A行转换为:
Test t2(t1);
因此,A行和B行都调用了完成拷贝的构造函数,初始化新的对象。
其实如果在Test类中不定义一个完成拷贝功能的构造函数,也照样没有问题。这是因为编译器会自动生成一个隐式的完成拷贝功能的构造函数:
Test(Test &t) { x = t.x; y = t.y; }
也就是说,由编译器为每个类产生的这种含有隐含的完成拷贝功能的构造函数,会依次完成类中对于成员数据的拷贝。但是在产生对象时,如果只要拷贝类中部分成员的数据,或者类中的某些成员是使用new运算符动态申请存储空间进行的,那么就必须在类中显式地定义一个完成拷贝功能的构造函数,以便正确完成拷贝。
前者是比较好理解,后者为什么成员new出来的就不行呢?比如:
#include <iostream> using namespace std; class String { private: char* Buffer; public: String(char* s) { if (s) { Buffer = (char*)new char[strlen(s) + 1]; strcpy_s(Buffer, strlen(s) + 1, s); } else Buffer = 0; cout << "调用了构造函数!" << endl; } String(String &s) { if (s.Buffer) { Buffer = (char*)new char[strlen(s.Buffer) + 1]; strcpy_s(Buffer, strlen(s.Buffer) + 1, s.Buffer); } else Buffer = 0; cout << "调用了拷贝功能的构造函数!" << endl; } ~String() { if (Buffer) { delete[]Buffer; } } }; int main() { String s1("Hello"); String s2(s1); //A cout << "退出主函数!" << endl; system("pause"); return 0; }
这段程序的运行结果是:
调用了构造函数! 调用了拷贝功能的构造函数! 退出主函数! 请按任意键继续. . .
当然,如果将拷贝功能的构造函数删掉,使用默认的。也就是:
String(String &temp){ Buffer = temp.Buffer; }
那这样的话,执行A句的时候,将s1的数据成员Buffer赋值给s2,那么s1和s2就只想同一个存放“Hello”字符串的地址。那么,在程序结束,调用析构函数的时候,一个地址被释放了两次,必然会引起运行错误。
同时,如果在main()函数改成:
int main() { String s1("Hello"); String s2(s1); //A s2 = s1; //B cout << "退出主函数!" << endl; system("pause"); return 0; }
尽管在A行能够成功赋值完成内容的拷贝,但是在B行的时候,还是讲s1和s2指向了用一个地址,析构函数也同样会出现问题。尽管s2=s1语法上没有错误。解决这个问题,需要用到运算符重载,以后会讲到。
也就是说,完成拷贝功能的构造函数是为了在初始化的时候,完成内容的拷贝,而不是地址的拷贝;“=”运算符重载是为了在赋值的时候,完成内容的拷贝,而不是地址的拷贝。
总结
- 实现类型转换的构造函数(用原本的构造函数--赋值):
x2 = 200; //隐式 x2 = Ex1(200); //显示
- 实现类型转换的构造函数的一般格式:无。
- 实现拷贝功能的构造函数(用新的构造函数--初始化):
String s2 = s1; //隐式 String s2(s1); //显式
- 完成拷贝功能的构造函数的一般格式为:
类名::类名(类名 &形参){ ... }
构造函数与对象成员
在定义一个新类时,可把一个已定义类的对象作为该类的成员。产生新定义类的对象时,须对它的对象成员进行初始化,且只能通过新类的构造函数来对它的所有成员数据初始化。对对象成员进行初始化,必须通过调用其对象成员的构造函数来完成。
例如:
#include <iostream> using namespace std; class A { private: int x, y; public: A(int a, int b) { x = a; y = b; cout << "调用A的构造函数!" << endl; } }; class B { private: int Length, Width; A a1; //A public: B(int a, int b, int c, int d):a1(c, d) { //B Length = a; Width = b; cout << "调用B的构造函数!" << endl; } }; int main() { B b1(1, 2, 3, 4); cout << "退出主函数!" << endl; system("pause"); return 0; }
这段程序的运行结果是:
调用A的构造函数! 调用B的构造函数! 退出主函数! 请按任意键继续. . .
也就是说,在B类中有A类的对象作为成员,那么在对B类的构造函数时,需要指定哪些参数是为了作为A类构造函数的参数。
在一个类中,说明对象成员的一般步骤为:
class 类名{ private: 已定义类名1 成员名1; 已定义类名2 成员名2; public: 类名(类名构造函数的参数列表):成员名1(已定义类名1的参数列表),成员名2(已定义类名2的参数列表){ ... } }
注意:在“:”后面为成员初始化列表,用“,”隔开的,“已定义类名1的参数列表”和“已定义类名2的参数列表”中的参数,其实都来自于“类名构造函数的参数列表”。并且在“类名构造函数的参数列表”中必须要有类型说明,而“已定义类名1的参数列表”和“已定义类名2的参数列表”无需提供类型说明。
这里的顺序问题:
- 如果一个类有几个对象成员,那么对对象成员的构造函数的调用顺序:取决于这些对象成员在类中说明的顺序,与它们在“类名构造函数的参数列表”和成员初始化列表中的顺序无关;
- 类的构造函数、类对象成员的构造函数的调用顺序:当建立起类的对象时,先调用各个对象成员的构造函数,初始化对应的对象成员,然后再去执行类的构造函数,初始化其他非对象成员的成员。析构函数的调用顺序与构造函数正好相反。
来一条练习题:
#include <iostream> using namespace std; class Obj { private: int val; public: Obj() { val = 0; cout << val << "调用Obj缺省的构造函数!" << endl; } Obj(int i) { val = i; cout << val << "调用Obj的构造函数!" << endl; } ~Obj() { cout << "调用Obj的析构函数!" << endl; } }; class Con { private: Obj one, two; //A int data; public: Con() { data = 0; cout << data << "调用Con缺省的构造函数!" << endl; } Con(int i, int j, int k):two(i+j),one(k) { //B data = i; cout << data << "调用Con的构造函数!" << endl; } ~Con() { cout << "调用Con的析构函数!" << endl; } }; int main() { Con c(100, 200, 400); cout << "退出主函数!" << endl; system("pause"); return 0; }
这段程序的运行结果是:
400调用Obj的构造函数! 300调用Obj的构造函数! 100调用Con的构造函数! 退出主函数! 调用Con的析构函数! 调用Obj的析构函数! 调用Obj的析构函数! 请按任意键继续. . .
注意一下顺序:当建立C的对象时,先调用其对象成员one、two的构造函数,而one和two的顺序取决于类中说明的顺序(A行,不是B行)。也就是说,先创建one对象、再two对象、再c对象。析构函数的顺序正好相反。
-
【C++】(二十一)默认构造函数和复制构造函数
2020-11-02 20:02:08默认构造函数就是在没有显式提供初始化式时调用的构造函数,它是一个不带参数的构造函数。如果定义某个类的对象时没有提供初始化式就会使用默认构造函数。 定义默认构造函数(default constructor)的一般形式为: ... -
C++拷贝构造函数、构造函数和析构函数
2018-08-30 22:09:15一、拷贝构造函数 ... 1、类对象的拷贝 对于普通类型的对象来说,它们之间的复制是很简单的,例如: int a=88;... 而类对象与普通对象不同,类对象内部结构一般较为...下面看一个类对象拷贝的简单例子。 #... -
C++11 继承构造函数
2018-08-15 14:43:53如果基类拥有多个构造函数,那么子类也需要实现多个与基类构造函数对应的构造函数。 class Base { public: Base(int va) :m_value(va), m_c(‘0’){} Base(char c) :m_c(c) , m_value(0){... -
构造函数的8种方式
2021-04-19 09:55:34没有绝对优秀的构造函数的方法,每一种方法都有他们的优缺点,我们需要考虑的是知道他们的应用场景,合理的使用他们,从而达到自己的要求。 1.Object构造函数模式 使用方式:先创建空对象,再添加属性/方法 适用场景... -
C#构造函数详解
2018-06-03 00:19:47构造函数 本文提供全流程,中文翻译。 Chinar 坚持将简单的生活方式,带给世人!(拥有更好的阅读体验 —— 高分辨率用户请根据需求调整网页缩放比例) Chinar —— 心分享、心创新!助力快速理解 构造... -
C/C++编程:继承构造函数
2021-04-29 11:39:49在传统C++中,如果派生类要使用使用基类的构造函数,通常需要在构造函数中显式声明: struct A{ public: A(int i) {} }; struct B : A{ B (int i) : A(i){} }; B派生自A,B又在构造函数中调用A的构造函数,从而... -
Vue 构造函数
2020-04-26 08:52:59我们知道,我们在使用Vue的时候,要使用new操作符进行调用,这说明Vue应该是一个构造函数,所以我们要做的第一件事就是:把Vue构造函数搞清楚。 #Vue 构造函数的原型 在了解 Vue 这个项目一节中,我们在最后提到这... -
C++ 之构造函数(拷贝/复制构造函数)
2020-06-26 12:52:51拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。拷贝构造函数通常用于: 通过使用另一个同类型的对象来初始化新创建的对象。 复制对象把它作为参数传递给... -
派生类构造函数规则
2015-01-19 16:21:12如果基类拥有构造函数但是没有默认构造函数,那么派生类的构造函数必须显示的调用基类的某个构造函数。 以B类从A类派生为例,我们总结如下: 1、若B有构造函数而A没有,当创建B类的对象时,B的相应构造函数被自动... -
JS中的构造函数
2021-01-16 16:53:40这样我们就可以通过一个构造函数创建多个对象,这些对象拥有相同的构造,都可以使用这个构造函数的方法和属性。 构造函数使用 构造函数的创建 function Dog(){ //构造函数中的属性 this.name = "贝贝"; //构造... -
Kotlin入门(12)类的概貌与构造
2017-08-11 09:45:13上一篇文章提到泛型函数appendString是在类外面定义,这不免使人疑惑,类里面又该怎样定义成员函数呢?为解答这个疑问,接下来的几篇文章将好好描述一下Kotlin如何操作类及其对象,本篇文章先对类的定义进行说明并... -
Python 面向对象 Point类的构造函数
2020-11-08 15:58:17Python acm试题 Point类的构造函数 下面展示一些 内联代码片。 // A code block var foo = 'bar'; class Point: __x = 0 __y = 0 def ShowPoint(self): print('({},{})'.format(self.__x,self.__y)) def ... -
C++ 构造函数与析构函数
2021-03-12 16:19:08如果类中没有定义构造函数,编译器在编译过程中会为类生成一个默认的无参构造函数,并不进行任何操作。 构造函数的意义:简化了对象的初始化工作,有了构造函数就不用专门再写初始化函数,也不用担心在生成对象时... -
彻底理解Dart的构造函数
2019-01-15 17:35:53如果您希望拥有更好的阅读体验,欢迎访问 我的开发笔记 Dart语言中的类 Dart 语言中所有的对象都是某一个类的实例,所有的类有同...使用new语句来构造一个类,构造函数的名字可能是ClassName,也可以是ClassName.o... -
什么是拷贝构造函数?拷贝构造函数何时被调用
2019-05-05 09:33:00可见,拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它的唯一的一个参数是本类型的一个引用变量,该参数是const类型,不可变的。例如:类X的拷贝构造函数的形式为X(X& x)。... -
C# 不包含采用0个参数的构造函数
2017-02-05 11:32:47一、构造函数是什么? (1)构造函数 ,是一种特殊的方法。构造函数具有与类相同的名称,它主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。不带参数的... -
Java Bean 为什么必须要有一个无参构造函数
2018-01-22 12:34:27JAVAEE中,javaBean是一个很重要的概念而JavaBean还有以下的使用习惯 首先我们知道,有两种方法为java对象中的元素赋值, 1. 通过类的带参数构造函数的方法 ... 对于一个实体类,对于多个参数,降低可读性 2... -
C++拷贝构造函数、赋值构造函数(深拷贝,浅拷贝)
2019-05-21 11:08:29拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它的唯一的一个参数是本类型的一个引用变量,该参数是const类型,不可变的。例如:类X的拷贝构造函数的形式为X(const X& x)。自定义拷贝构造... -
Th3.14:对象的移动、移动构造函数、移动赋值运算符函数
2021-11-18 20:54:16本博客将记录:新经典课程知识点的第14节的...在以往的coding过程中,有时对于一个类的对象do大量的对象拷贝和赋值操作都是非常消耗性能的!因此C++11中提出了“对象移动”的操作。那么什么叫做“对象移动”呢? ... -
C++类使用构造函数初始化类表和构造函数函数体中赋值的区别
2015-06-19 10:30:04C++ Primer中在讲构造函数初始化列表的时候有这么一段话: 无论是在构造函数初始化列表中初始化成员,还是在构造函数体中对它们赋值,最终结果是相同的。不同之处在于,使用构造函数初始化列表的版本初始化数据... -
创建一个学生类,成员属性为学号,姓名,性别,年龄,java,成绩。成员属性通过构造方法
2020-12-23 11:49:51展开全部public class Student {//1、定义成员变量private String no;//学号private String name;//姓名private String sex;...//成绩//2、通过构造方法初始化成32313133353236313431303231363533e5... -
类继承中构造函数和析构函数的调用
2016-07-02 15:28:46类继承中构造函数和析构函数的调用 现在,有三个类,类的定义如下 class CA { public: CA(){cout ~CA(){cout }; class CB:public CA { public: CB(){cout ~CB(){cout }; ... -
C++11 委托构造函数
2018-08-15 14:41:39委托构造函数(Delegating Constructor)由C++11引入,是对C++构造函数的改进,允许构造函数通过初始化列表调用同一个类的其他构造函数,目的是简化构造函数的书写,提高代码的可维护性,避免代码冗余膨胀。... -
组合类构造函数、析构函数详解
2014-10-18 16:25:47组合类的概念就是:指在一个类的数据成员中含有一个或多个类的对象,拥有这样结构的类就叫组合类。 这种以数据成员身份出现的类对象就叫子对象。 继承和组合都可以实现一个类重用另一个类的行为功能,那么如何... -
构造函数为私有,如何创造一个类
2010-10-05 17:09:00构造函数为私有成员,如何去创建一个类 -
类和原型、构造函数及扩充
2019-01-05 00:37:06每个JavaScript对象都是一个属性集合,相互之间没有任何联系。在JavaScript中也可以定义对象的类,让每个对象都共享某些属性,这种...例如,假设有一个名为Complex的类用来表示复数,同时还定义了一些复数运算。...