创建对象
在C中,当使用
Studet stu;
创建对象时,创建的对象在栈中。使用Student *pStudent = new Student
创建时,对象在堆中,需要手动释放。
在堆上创建对象是匿名的,必须要有一个指针指向它,否则无法使用。而在栈上创建对象,都用名字,不必须用指针指向它,由系统管理内存。
访问对象
通过对象名字访问对象变量用.
(上文对象在栈中),通过指针对象访问对象变量用->
(上文对象在堆中)。
函数定义
在类体内定义函数会自动成为内联函数(大多时候并不需要),而定义在类体外并不会。
内联函数并不是我们所期望的,因此在类内进行生命,而在类外进行定义(实现),是一般我们所使用的。
而在类外定义函数的时候,注意要加上域解析符:
void Student::say(){
}
类成员的访问权限
- public: 公有的,在类的外部可以通过对象来访问成员(方法或变量)。
- private: 私有的,通常声明为
m_name
、m_age
,无法在类外访问。 - protected: 受保护的,
m开头修饰成员变量是约定俗成的写法 ,并非是语法限定。在C++中方便区分成员变量,以及和成员函数中的同名的形参区别开。
public
和private
可以用于封装类,将想要作为借口暴露的成员变量定义为public
,而将私有的不想暴露的成员变量定义为private
,如果想要在类的外部设置或者取出私有的成员变量,则通过定义一个public
的set
或get
方法间接的进行访问即可。而在OC
中直接将想要暴露的成员变量和函数声明在.h
文件的interface
中,而将不想暴露的成员变量声明在.m
的interface
中。
C++对象的内存
使用sizeof
来确定内存的大小。
sizeof(stu)
sizeof(*pStu)
sizeof(Student)
栈上创建的对象内存,堆上创建的内存对象,类上创建的内存对象都是12。
12是3个成员变量,每个占4字节,一共12字节。
编译器会将成员变量和成员函数分开储存。分别为每个对象的成员变量单独分配内存,但是所有对象都共用一段函数代码。
C++函数的编译
从上文可以看到,对象的内存只保留了成员变量,没有保留其属于Student
类型,也不知道他有个成员函数setName()
,那么如何通过对象调用成员函数呢?
C++和C中的函数编译不一样。C中只是函数前加下划线,而C++中的函数会在编译时根据它所在的命名空间、所属的类以及参数列表等信息,重新形成一个函数名。这个函数名只有系统知道,不开放给用户。这个重命名过程被称为名字编码(Name Mangling),可逆。
成员函数的调用
上文可以看到,成员函数最终被编译成了与对象无关的名字唯一的全局函数。如果直接调用没有问题。但是如果成员函数中使用了成员变量,已经变为了全局函数如何访问成员变量。
为了解决这个问题,C++在编译函数时要额外添加一个参数进去:当前对象的指针,通过指针来访问成员变量。
例如一个简单的函数:
void Demo::display(){
cout<<a<<endl;
}
那么编译后会类似转换为:
void new_function_name(Demo * const p){
cout<<p->a<<endl;
}
使用obj.display()
调用函数也会变为new_function_name(&obj)
。
const表示指针不能修改,p只能指向当前对象,不能指向其他对象。
通过这种方式完成了成员函数与成员变量的关联。这与表面上看到的相反,通过对象调用成员函数时,并不是通过对象找成员函数,而是通过函数找对象!
构造函数
系统会默认生成构造函数。
使用Student *pStu = new Student()
或者Student *pStu = new Student
来调用系统生成的默认构造函数。
如果用户自定义了构造函数,则系统不再生成默认的构造函数。
Student::Student(char *name, int age, float score){
m_name = name;
m_age = age;
m_score = score;
}
构造函数初始化列表
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){
}
相当于在函数内部执行了m_name=name
等操作。
此方法没有任何效率上的优势,仅仅是书写方便。
初始化const成员变量
class VLA {
private:
const int m_len;
}
VLA::VLA(int len): m_len(len){
}
使用const修饰的成员变量只能用构造函数初始化列表的方式赋值。
析构函数
析构函数(destructor)是一种特殊的成员函数,没有返回值,不需要也无法显式调用,会在对象销毁时自动执行。析构函数的名字,是在类名前加一个~
。
析构函数没有参数,不能被重载,一个类只有一个,如果没有定义,系统会自动生成。
在函数内部不使用new创建的对象是局部对象,位于栈区,会在函数结束时调用这些对象的析构器。而new创建的对象位于堆,只有使用delete时,析构器才会执行。
数组对象
数组中的每个元素都是对象,称为数组对象。
成员对象和封闭类
一个类的成员变量如果是另一个类的对象,称之为成员对象,包含成员对象的类叫封闭类(enclosed class)。
成员对象的初始化
创建封闭类的对象时,它包含的成员对象也需要被初始化,这就要使用封闭类构造函数的初始化列表
Car::Car(int price, int radius, int width): m_price(price), m_tyre(radius, width)/*指明 m_tyre 对象的初始化方式*/{ };
Car
是一个封闭类,初始化的同时,也初始化其成员对象Tyre
(初始化参数radius, width)。
封闭类对象生成时,先执行所有成员对象的构造函数,再执行封闭类自己的构造函数。成员对象的构造函数的执行次序和成员对象在类定义中次序一致,与在初始化列表中出现出现的次序无关。
成员对象的释放
释放顺序与构造顺序刚好相反,后构造的先释放。先执行封闭类的析构函数,再执行成员对象的析构。成员对象的析构与构造顺序相反,与定义顺序相反。
this
this 是C++中的关键字,也是一个const指针,指向当前对象,通过它可以访问当前对象的所有对象。
this实际上是成员函数的一个形参,在调用成员函数时将对象的地址作为实参传递给this。不过这个形参是隐式的,它并不出现在代码中,而是在编译阶段由编译器加入参数列表中。
上文提到成员函数最终会被编译成与对象无关的普通函数,除了成员变量,会丢失所有信息,所以编译时要在成员函数中添加一个额外的参数,把当前对象的首地址传入,以此关联成员变量和成员函数。这个额外的参数,实际就是this,它是关联成员函数和成员变量的桥梁。
static 静态成员变量
静态成员变量属于类,不属于某个具体的对象。即时创建多个对象,也共有一个静态成员变量,其中一个对象修改这个静态成员变量,其他对象访问也会变。
type class::name = value;
type 是变量的类型,class 是类名,name 是变量名,value 是初始值。将上面的 m_total 初始化
int Student::m_total = 0;
static成员变量的内存不在定义和创建对象时分配,而在类外初始化时分配。只能在类外进行。
- 通过类访问
Student::m_total = 10;
- 通过对象访问
Student stu("小明", 15, 92.5f);
stu.m_total = 20;
- 通过对象指针访问
Student *pstu = new Student("李华", 16, 96);
pstu -> m_total = 20;
static成员变量不占用对象内存,而是和普通static变量一样在对象外分配内存(在全局数据区分配内存),即使不创建对象也可以访问。
匿名对象
(new Student("小明", 15, 90)) -> show()
。
只使用了show()
方法,使用匿名对象。使用匿名对象无法回收内存,会造成内存泄漏,不建议使用。
静态成员函数
在类中,static
除了可以声明静态成员变量,还可以声明静态成员函数。普通函数可以访问所有所有成员(包括成员变量和成员函数),而静态函数只能访问静态变量(包括静态成员变量和静态成员函数).
const
在类中,如果不希望某些数据被修改,使用const关键字修饰。
const成员变量
const 可以用来修饰成员变量和成员函数。const 成员变量的用法和普通 const 变量的用法相似,只需要在声明时加上 const 关键字。初始化 const 成员变量只有一种方法,就是通过构造函数的初始化列表
const成员函数
const成员函数,也被称为常成员函数。
通常将get函数设置为常成员函数,常成员函数需要在声明和定义函数头部结尾处添加const
关键字。
区分const关键字的位置
函数开头const用来修饰返回值,表示返回值不能被修改。例如const char * getname()
函数头部结尾加const
用来表示是常成员函数。这种函数只能读取成员变量的值,而不能改变。
const对象
const也可以用来修饰对象,被称为常对象。一旦将对象定义为常对象,就只能调用类的const成员(const成员变量和const成员函数)。
友元函数和友元类
private为私有属性方法,不能被外部访问,但可以被friend
修饰的友元函数访问。
在当前类外定义的, 不属于当前类的函数,也可以在类中声明,不过要加friend
关键字声明为友元函数。这个友元函数不属于该类,但是可以访问其private的变量和方法。
void show(Student *pstu){
cout<<pstu->m_name<<"的年龄是 "<<pstu->m_age<<",成绩是 "<<pstu->m_score<<endl
}
int main(){
Student stu("小明", 15, 90.6);
show(&stu); //调用友元函数
Student *pstu = new Student("李磊", 16, 80.5);
show(pstu); //调用友元函数
return 0;
}
将其他类的成员函数声明为友元函数:
class Address{
public:
friend void Student::show(Address *addr);
}
是将Student
中的show
方法声明为Address
的友元函数,借此让其访问Address
中的私有属性。
还可以将一个类声明为另一个类的友元类,友元类的所有函数都是友元函数。
class Address{
public:
//将 Student 类声明为 Address 类的友元类
friend class Student;
}
类的作用域
PCHAR A::show(PCHAR str){
}
上面定义错误,PCHAR
在A::
外,不被识别。
A::PCHAR A::show(PCHAR str){
}
总结
- 类的成员有成员变量和成员函数两种。
- 成员函数之间可以互相调用,成员函数内部可以访问成员变量。
- 私有成员只能在类的成员函数内部访问。默认情况下,class 类的成员是私有的,struct 类的成员是公有的。
- 可以用“对象名.成员名”、“引用名.成员名”、“对象指针->成员名”的方法访问对象的成员变量或调用成员函数。成员函数被调用时,可以用上述三种方法指定函数是作用在哪个对象上的。
- 对象所占用的存储空间的大小等于各成员变量所占用的存储空间的大小之和(如果不考虑成员变量对齐问题的话)。
- 定义类时,如果一个构造函数都不写,则编译器自动生成默认(无参)构造函数和复制构造函数。如果编写了构造函数,则编译器不自动生成默认构造函数。一个类不一定会有默认构造函数,但一定会有复制构造函数。
- 任何生成对象的语句都要说明对象是用哪个构造函数初始化的。即便定义对象数组,也要对数组中的每个元素如何初始化进行说明。如果不说明,则编译器认为对象是用默认构造函数或参数全部可以省略的构造函数初始化。在这种情况下,如果类没有默认构造函数或参数全部可以省略的构造函数,则编译出错。
- 对象在消亡时会调用析构函数。
- 每个对象有各自的一份普通成员变量,但是静态成员变量只有一份,被所有对象所共享。静态成员函数不具体作用于某个对象。即便对象不存在,也可以访问类的静态成员。静态成员函数内部不能访问非静态成员变量,也不能调用非静态成员函数。
- 常量对象上面不能执行非常量成员函数,只能执行常量成员函数。
- 包含成员对象的类叫封闭类。任何能够生成封闭类对象的语句,都要说明对象中包含的成员对象是如何初始化的。如果不说明,则编译器认为成员对象是用默认构造函数或参数全部可以省略的构造函数初始化。
- 在封闭类的构造函数的初始化列表中可以说明成员对象如何初始化。封闭类对象生成时,先执行成员对象的构造函数,再执行自身的构造函数;封闭类对象消亡时,先执行自身的析构函数,再执行成员对象的析构函数。
- const 成员和引用成员必须在构造函数的初始化列表中初始化,此后值不可修改。
- 友元分为友元函数和友元类。友元关系不能传递。
- 成员函数中出现的 this 指针,就是指向成员函数所作用的对象的指针。因此,静态成员函数内部不能出现 this 指针。成员函数实际上的参数个数比表面上看到的多一个,多出来的参数就是 this 指针。
网友评论