C++:面向对象基础
构造函数
C++
中有三种构造函数:默认构造函数,有参构造函数,拷贝构造函数
class Person{
public:
int m_age;
public:
//默认构造函数
Person(){
}
//有参构造函数
Person(int age){
m_age = age;
}
//拷贝构造函数(拷贝构造函数必须加const修饰)
//必须是引用类型,且需要const修饰
Person(const Person& p){
m_age = p.m_age;
}
}
类对象的初始化
-
括号法
//默认构造函数不要加小括号,因为编译器会认为这是一个函数的声明。 //Person p(); //调用默认构造函数进行初始化 Person p1; //有参构造函数初始化 Person p2(100); //调用拷贝构造函数进行初始化 Person p3(p2);
-
显示调用法
//使用默认构造函数初始化 Person p1 = Person(); //使用有参构造函数初始化 Person p2 = Person(10); //使用拷贝构造函数初始化 Person p3 = Person(p2); //使用拷贝构造函数初始化(发生隐式转换) person p4 = p3; //匿名对象,该段代码运行完毕后就会被释放 Person(20); //不能使用拷贝构造函数初始化匿名对象 //编译器在编译时会将括号去掉形成如下形式 //person p1; //这将引发一个重定义的错误 Person(p1);
什么情况下会发生拷贝构造调用生成对象
-
用已经创建好的对象来初始化新的对象
Person p1 = Person(p);
-
以值传递的方式给函数参数传值
//值传递,Person p = Person(p1) void testPerson01(Person p){ } void test(){ person p1; //值传递 testPerson(p1); }
-
以值的方式返回局部对象,因为函数返回不是以引用的方式返回,而是以值的方式返回,因此返回的是一个拷贝后的匿名对象。
Person testPerson02(){ Person p1; return p1; } void test(){ //老版本的编译器会在函数返回时,调用拷贝构造对p进行赋值。 Person p = testPerson02(); }
现在的编译器会对上面的代码进行优化,不会再出现拷贝构造调用。类似于如下优化
void testPerson02(Person& p1){
p1 = Person(); //调用构造函数
}
void test(){
//并不会调用构造函数
Person p;
//优化到函数内部,调用构造函数,这样优化可以减少CPU和内存开销
testPerson02(p);
}
编译器会给一个类提供哪些默认的函数
编译器会给一个类提供三个默认的函数: 1. 默认的构造函数 2.拷贝构造函数 3.析构函数4. operator=运算符函数(简单的值传递)。编译器提供的默认拷贝构造函数会进行简单的值拷贝。
当提供了有参构造函数,编译器就不会提供默认的无参构造函数,但是默认的拷贝构造函数编译器还会提供。如果提供了拷贝构造函数,编译器将不会提供任何其他的默认构造函数。
使用初始化列表进行初始化
class Person{
private:
int m_age;
string m_name;
public:
//默认无参构造函数
Person(){}
//初始化列表实现类对象构造
Person(int age , string name)
:m_age(age), m_name(name){
}
}
类的构造和析构的顺序
类对象作为成员时,构造顺序是:先将类对象按着在类中声明的先后顺序进行一一构造,然后在构造自己。而析构的顺序与构造顺序相反。
explicit关键字
class MyString {
private:
int length;
public:
//加上explicit关键字则下面2无法编译通过
//表示不允许使用隐式类型转换构造对象
explicit MyString(int size) {
length = size;
}
};
void test() {
MyString s1(10); //----------1
MyString s2 = 10;//-----------2
}
对象的动态创建和销毁
C++
使用新的关键字new
来分配对象到堆空间。使用delete
关键字释放对象的空间。
注意:
Person *person = new Person(); //动态分配了一个Person对象的内存
delete person; //正确释放了person的内存空间
//下面的写法需要避免
void *person2 = new Person();//动态分配了一个Person对象的内存
delete person;//无法释放内存空间,析构函数不会执行,释放失败
delete (Person*)person;//正确释放
//动态创建数组
Person *personArray = new Person[10];
//使用delete[]释放
delete[] personArray;
表明了delete
释放内存需要正确知道释放的内存空间的地址是什么类型的,否则无法正确完成内存的释放。
当我们使用在堆上为对象分配数组空间时,一定要为对象提供默认的构造函数。当我们为一个对象分配了内存空间时,不能用void*来接收,因为类型不明确时delete将无法释放对象内存空间。
new
和malloc
的区别,new
会调用构造函数而malloc
不会,同样delete
会调用析构函数而free
不会。当使用动态创建数组时,对象必须有默认的无参构造函数。使用new
创建的数组空间,必须要使用delete[]
来释放,因为如果使用free
释放,则不会调用数组中各个对象的析构函数,可能导致内存泄漏。
静态成员变量
在一个类中,若将一个成员变量声明为static
,这种成员变量称为静态成员变量。与一般的数据成员不同,无论建立多少个对象,都只有一个静态数据的拷贝。静态成员变量属于某个类,所有对象共享。
静态变量是在编译阶段就分配内存空间,对象还没创建时就已经分配空间。
- 静态变量必须在类中声明,在类外定义。
- 静态数据成员不属于某个对象,在为对象分配空间中不包括静态成员所占空间
- 静态数据成员可以通过类名或者对象名来引用。
class Person
{
public:
Person();
~Person();
private:
static int age;
};
//类里声明,类外定义,定义时有类型和类名作用域。
int Person::age = 10;
void test01() {
//编译报错,因为age是私有的,表示static变量也是有访问权限的
int age = Person::age;
}
C++不允许在类里初始化(静态成员变量)类成员变量,因为类成员变量,可以直接工作类名调用,在编译阶段就分配好内存,而在类里直接初始化,一般是在类构造阶段。因此一般类成员变量,类里声明,类外定义。私有权限的类成员变量,在类外当初始化时可以初始化,但使用时不能访问。
C语言中的static
C/C++语言代码是以文件为单位来组织的,在一个源程序的所有文件中,一个外部变量或者函数只能在一个源程序中定义一次。如果重复定义的话编译器就会报错,随着不同源文件的变量和函数之间的相互引用以及相互独立的关系,于是就产生了extern和static关键字。
- static全局变量
.text段
保存进程所执行的程序二进制文件,.data段
保存进程所有已初始化的全局变量,.bss段
保存进程未初始化的全局变量(其他段中还有很多乱七八糟的段,暂且不表述)。在进程的整个生命周期中,.data段
和.bss段
内的数据是与整个进程同生共死的,也就是在进程结束之后这些数据才会寿终就寝。
当一个进程的全局变量被声明为static
之后,它的中文名叫静态全局变量。静态全局变量和其他的全局变量的存储地点并没有区别,都是在.data段
(已初始化)或者.bss段
(未初始化)内,但是它只在定义它的源文件内有效,其他源文件无法访问它。所以,普通全局变量穿上static
外衣后,它就变成了新娘,已心有所属,只能被定义它的源文件(新郎)中的变量或函数访问。
- static局部变量
普通的局部变量在栈空间上分配,这个局部变量所在的函数被多次调用时,每次调用这个局部变量在栈上的位置都不一定相同。局部变量也可以在堆上动态分配,但是记得使用完这个堆空间后要释放之。
static
局部变量中文名叫静态局部变量。它与普通的局部变量比起来有如下几个区别:
- 位置:静态局部变量被编译器放在全局存储区.data(注意:不在.bss段内,原因见3),所以它虽然是局部的,但是在程序的整个生命周期中存在。
- 访问权限:静态局部变量只能被其作用域内的变量或函数访问。也就是说虽然它会在程序的整个生命周期中存在,由于它是static的,它不能被其他的函数和源文件访问。
- 值:静态局部变量如果没有被用户初始化,则会被编译器自动赋值为0,以后每次调用静态局部变量的时候都用上次调用后的值。这个比较好理解,每次函数调用静态局部变量的时候都修改它然后离开,下次读的时候从全局存储区读出的静态局部变量就是上次修改后的值。
-
static函数
C++面向对象编程中的private函数,私有函数只有该类的成员变量或成员函数可以访问。在C语言中,也有“private函数”,它就是接下来要说的static函数,完成面向对象编程中private函数的功能。
当你的程序中有很多个源文件的时候,你肯定会让某个源文件只提供一些外界需要的接口,其他的函数可能是为了实现这些接口而编写,这些其他的函数你可能并不希望被外界(非本源文件)所看到,这时候就可以用static修饰这些“其他的函数”。
所以
static
函数的作用域是本源文件,把它想象为面向对象中的private
函数就可以了。
C++面向对象模型
class Person{};
int size = sizeof(Person); //结果是1
C++中空类会占一个字节,这是为了让对象的实例能够相互区别。具体来说,空类同样可以被实例化,并且每个实例在内存中都有独一无二的地址,因此,编译器会给空类隐含加上一个字节,这样空类实例化之后就会拥有独一无二的内存地址。如果没有这一个字节的占位,那么空类就无所谓实例化了,因为实例化的过程就是在内存中分配一块地址。
空指针调用成员函数
空指针是可以调用成员函数的,但是前提是这个成员函数内部不能有成员属性,因为成员属性实际上是通过this
指针来调用的,当this
指针为空时,则调用就会出错。
常对象和常函数
class Person{
private:
int age;
public:
void showAge1() const{ //常函数
//常函数中的const实际上修饰的是const *this
//再常函数中无法修改对象的属性值
this->age = 20;
cout<<" age = "<< age << endl;
}
void showAge2(){
//此处可能会修改age的值
this->age = 20;//成功
}
};
void test(){
const Person person; //常对象
person.showAge1(); //调用成功
//调用失败,应为改函数不是一个常函数,其内部可能会修改person的属性
person.showAge2();
}
友元函数
友元函数的目的就是为了让外部函数能访问到该类对象的私有属性。
class Woman
{
//声明时需要加上friend关键字,参数需要声明当前类的对象指针
friend int getAge(Woman *woman);
public:
std::string name;
private:
int age;
};
//定义时friend可加可不加
int getAge(Woman *woman) {
woman->age;
}
友元类
正常情况下,当一个类A里持有另一个类B的对象,那么是无法通过B这个对象直接访问B里的私有成员。那么如果我们想要在A类里可以无任何限制的通过B这个对象访问到B对象里的私有成员,该如何做呢?此时我们只需要告诉B,A这个类是他的好朋友就行了。
class Woman
{
//将Bosom类声明为这个类的友元类
friend class Bosom;
public:
std::string name;
private:
int age;
};
class Bosom {
public:
Bosom() {
}
public:
int getBosomAge() {
int age = girl.age;
return age;
}
private:
Woman girl;
};
- 友元关系不能被继承
- 友元关系是单向的,类A是类B的朋友,但是累B不一定是类A的朋友。
- 友元关系不具传递性,类B是类A的朋友,类C是类B的朋友,但类C不一定是类A的朋友。