C++学习记录
类和对象
类是现实世界或思维世界在实体计算机中的反映,它将数据以及这些数据上的操作封装在一起。对象是具有类型的变量。
对象是类的实例,类是对象的模板。
举一个简单的例子,人类与张三,人类就代表一个类,而张三就是人类里面的一个实例。
创建一个类
在创建一个类时,我们常采用class
+类名创建,如:
class Student{
//访问权限
public:
//属性
string m_name;
int m_age;
//行为
void showName()
{
cout << m_name << endl;
}
void showAge()
{
cout << m_age << endl;
}
};
这样一来我们的类就创建完成了。那么如何根据类创建一个对象呢?
Student stu;
stu.m_name = "张三";
stu.m_age = 18;
这样一来我们就有一个名为张三的对象了。还记得创建类时我们包括了行为这一部分吗?我们可以直接调用类里面的函数:
stu.showName();
stu.showAge();
这样我们就会打印出张三的名字和年龄了。
那看到这里有的小伙子就会问了:啊这玩意和结构体有什么区别啊。不要着急,下面我就来介绍类和结构体之间的区别。
类和结构体的区别
我们在上面创建类时,是不是有一个访问权限?类和结构体的主要区别就在于这个访问权限上。
说到访问权限,我们先要明确类有三种访问权限:public
、private
、protected
,这三种的区别在于:
public
:类内类外都可以访问。
private
:类内可以访问,类外不可以访问。
protected
:类内可以访问,类外不可以访问。
那看到这里有的小伙子就会说了,private
和protected
到底有什么区别啊,这个在目前的学习中我们暂时用不到,我们只需要知道他们是有区别的就行了。
再回到类和结构体的区别:类的访问权限默认是private
,而结构体的访问权限则默认是public
。这也就是他们哥俩的主要区别。
深拷贝和浅拷贝
我们知道当我们没有写拷贝构造函数的时候,编译器会自动帮我们写一个拷贝构造函数,这就叫做浅拷贝。
那么我们的深拷贝又是什么呢?我们先从浅拷贝的一些问题说起:
在涉及堆区开辟数据时,浅拷贝会造成内存的重复释放
为什么会这么说呢?因为编译器默认实现的代码是:
int * m_height;
Person(int age,int height)
{
m_age = age;
m_height = new int(height);
cout << "这是有参构造函数的调用" << endl;
}
Person(const Person & p)
{
cout << "这是拷贝构造函数的调用" << endl;
m_age = p.m_age;
m_height = p.m_height; //这是编译器默认实现的代码
}
这样一来,我们被拷贝的函数里的m_height
指向了在堆区new
出的一块空间,而编译器默认实现的拷贝构造函数中的m_height
与被拷贝的函数的m_height
相等,也就是说:两个指针指向了堆区的同一块内存空间。
这样一搞就不得了了,我们的析构函数的使命就是在函数运行完毕后将堆区开辟的数据释放掉:
//析构函数作用:将堆区开辟的数据释放
~Person()
{
if (m_height != NULL)
{
delete m_height;
m_height = NULL;
}
cout << "这是析构函数的调用" << endl;
}
而析构函数的调用必然有先有后,由于栈结构是“先进后出”,那么我们肯定是先调用拷贝构造函数的析构函数,OK,莫得问题,但当拷贝构造函数内的m_height
所指向的空间释放之后,我们被拷贝的函数也要释放吧?但此时堆区的空间已被释放过了,再次释放就会造成重复释放了。
那么我们如何解决这一问题呢?这就轮到了深拷贝出场了:
Person(const Person & p)
{
cout << "这是拷贝构造函数的调用" << endl;
m_age = p.m_age;
//m_height = p.m_height; //这是编译器默认实现的代码,会造成内存重复释放
m_height = new int(*p.m_height);
}
我们在写拷贝构造函数时,涉及指针的部分我们另在堆区开辟一块空间不就行了吗?这样,拷贝构造函数中的m_height
指向这一块堆区,被拷贝的构造函数指向另一块堆区,这样我们就解决了重复释放的问题啦!
静态成员变量和静态成员函数
静态成员变量
在类内定义static
成员变量时,有三个特点:
1.所有对象都共享同一份数据
2.编译阶段就分配内存
3.类内声明,类外初始化操作
我们针对第三点做出说明,究竟什么才叫类内声明,类外初始化操作呢?
我们通过下段代码做出具体解释:
class Person{
public:
static int m_A;
};
int Person::m_A = 10;
上面的static int m_A
属于在类内声明了m_A,而类外初始化操作则是通过:int Person::m_A = 10;
这段代码实现的。
此外,静态成员变量还可以通过类名直接访问:
cout << Person::m_A << endl;
静态成员函数
上面我们介绍了静态成员变量,与之对应的就是我们的静态成员函数了。
1.所有对象共享同一个函数
2.静态成员函数只能访问静态成员变量
针对第二点,我们看以下代码:
class Person{
public:
static int m_A;
int m_B; //error
static void func()
{
m_A = 200;
m_B = 200;//error
cout << "这是静态成员函数的调用" << endl;
}
};
int Person::m_A = 10;
由于m_B
是非静态成员变量,因此静态成员函数不能访问m_B
。
成员变量和成员函数
空对象
空对象占用1个内存空间
C++编译器会给每个空对象也分配一个字节空间,是为了区分空对象占内存的位置
每个空对象也应该有一个独一无二的内存地址
具体代码如下:
class Person{
};
void test ()
{
Person p;
cout << sizeof(p) << endl;
}
上述输出结果为1,证明空对象只占用1个内存空间。
成员变量和成员函数是分开存储的
class Person{
int m_A; //非静态成员变量,属于类的对象上
static int m_B; //非静态成员变量,不属于类对象上
void func1() //非静态成员函数,不属于类对象上
{
}
static void func2() //静态成员函数,不属于类的对象上
{
}
};
void test ()
{
Person p;
cout << sizeof(p) << endl;
}
上述输出结果为4,4为int
类型所占的内存空间。
由此我们得出一个结论:
1.空对象占用的内存空间为1。
2.只有非静态成员变量属于类的对象上,其他的都不属于类的对象。也就是说,在计算类所占用的内存空间时,我们只需要统计非静态成员变量所占的内存空间即可。
this指针
通过上面我们知道在C++中成员变量和成员函数是分开存储的,每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码。
那么问题是:这一块代码是如何区分那个对象调用自己的呢?
c++通过提供特殊的对象指针,this指针,解决上述问题。this指针指向被调用的成员函数所属的对象
this指针是隐含每一个非静态成员函数内的一种指针
this指针不需要定义,直接使用即可
this指针的本质是一个指针常量,即指针的指向不可修改
this指针的用途:
- 当形参和成员变量同名时,可用this指针来区分
- 在类的非静态成员函数中返回对象本身,可使用return *this
class Person
{
public:
Person(int aclass Person
{
public:
Person(int age)
{
//1、当形参和成员变量同名时,可用this指针来区分
this->age = age;
}
Person& PersonAddPerson(Person p)
{
this->age += p.age;
//返回对象本身
return *this;
}
int age;
};
void test01()
{
Person p1(10);
Person p2(10);
p2.PersonAddPerson(p1).PersonAddPerson(p1).PersonAddPerson(p1);
cout << "p2.age = " << p2.age << endl;
}
输出结果为40.
Tips:当使用值的方式返回对象,返回的是一个新对象;当使用引用的方式返回,返回的就是原对象。
这也解释了为什么我们在
Person & PersonAddPerson(Person p)
{
this->age += p.age;
//返回对象本身
return *this;
}
要用Person & PersonAddPerson(Person p)
而不是PersonPersonAddPerson(Person p)
。
因为如果使用第二种Person PersonAddPerson(Person p)
,我们的
p2.PersonAddPerson(p1).PersonAddPerson(p1).PersonAddPerson(p1);
中的p2.PersonAddPerson(p1)
返回的就是p2'
,而p2.PersonAddPerson(p1).PersonAddPerson(p1)
返回的是p2''
,整个函数返回的是p2'''
,跟p2
完全不是一个东西,所以结果是20。
const修饰成员函数
常函数
在修饰成员函数时,我们可以通过const
修饰使成员函数内的变量值不可以发生改变:
int m_A;
mutable int m_B;
void showPerson () const
{
this->m_A = 100; //error
this->m_B = 100; //OK
}
由于
this
指针本质上是一个指针常量Person * const this
,也就是说一旦某个对象调用了showPerson
函数,this
就指向该对象并且不能再改变指向直至函数调用完毕。而我们想让函数的值也不能被改变,这时我们就可以使用const Person * const this
来让this
所指向对象的值也不发生改变,那么这个const
加在哪呢?就加在我们函数括号后面吧!
如果我们想让函数内某一个变量可以被改变,那么我们只需要在定义这个变量时在最前面加上mutable
即可,如mutable int m_B
,这样我们的变量就可以被修改了。
常对象
常对象和常函数同理,在定义常对象时只需加上一个const
即可:
const Person p;
常对象只能修改mutable
修饰的变量,只能调用常函数。
因为普通成员函数可以修改属性,而常函数不能修改,如果常对象能够调用普通成员函数,就相当于变相的修改了属性。
友元
全局函数做友元
在前面我们提到了在类中定义的一些私有的属性在类外我们是访问不到的,那么假如在类外就是有这么一个函数偏偏要访问这些私有属性怎么办呢?这就引出了我们“友元”的概念,毕竟只有好基友才能访问私有的属性嘛!
友元提供了一种 普通函数或者类成员函数 访问另一个类中的私有或保护成员 的机制。也就是说有两种形式的友元:
(1)友元函数:普通函数对一个访问某个类中的私有或保护成员。
(2)友元类:类A中的成员函数访问类B中的私有或保护成员
优点:提高了程序的运行效率。
缺点:破坏了类的封装性和数据的透明性。
总结: - 能访问私有成员 - 破坏封装性 - 友元关系不可传递 - 友元关系的单向性 - 友元声明的形式及数量不受限制
在类内的任何一个位置,我们把函数进行声明,并在声明的最前面加上
friend
多说无益,我们来看示例:
class Friend{
....
friend void showFriend();
....
};
这样,我们的showFriend
函数也能访问到被设置为private
类型的成员属性了。
类做友元
class Friend{
...
friend class goodGay;
...
};
这样一来,我们的goodGay
类就可以访问到Friend
类内的私有属性。
成员函数做友元
假如我们想让goodGay
中的某些成员函数能够访问Friend
类内的私有属性,我们该怎么做呢?
class goodGay{
public:
void visit1();
void visit2();
};
class Friend{
friend void goodGay::visit1();
...
};
我们只需像上面这样在Friend
类内做一个声明即可。
注意:这个声明不但要加上friend
,还要指明是哪个类内的成员函数(goodGay::xxxx)
Tips
- **友元关系没有继承性。**假如类B是类A的友元,类C继承于类A,那么友元类B是没办法直接访问类C的私有或保护成员。
- **友元关系没有传递性。**假如类B是类A的友元,类C是类B的友元,那么友元类C是没办法直接访问类A的私有或保护成员,也就是不存在“友元的友元”这种关系。
运算符重载
概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型
简单地说,我们的 +
, -
, *
, /
等运算符只能对一些符合默认规则的运算起作用,比如 int + int
、 double * double
等,假如我们想对类进行这些运算,显然是行不通的,于是就引入了运算符重载这个概念。
重载运算符是一个带有特殊名称的函数,函数名是由关键字 operator
和其后要重载的运算符符号构成的(如:operator +
)。与其他函数一样,重载运算符有一个返回类型和一个参数列表。
加法重载运算符
实现两个自定义数据类型相加的运算
成员函数实现加法运算符重载
class Person{
public:
Person(int a, int b)
{
this->m_A = a;
this->m_B = b;
}
//成员函数实现加法运算符重载
Person operator+(const Person & p)
{
Person temp;
temp.m_A = this->m_A + p.m_A;
temp.m_B = this->m_B + p.m_B;
return temp;
}
int m_A;
int m_B;
};
void test01()
{
Person p1(10, 10);
Person p2(10, 10);
Person p3 = p1 + p2; //相当于p1.operator + (p2)
//结果为 p3.m_A = 20;
// p3.m_B = 20;
}
全局函数实现加法运算符重载
class Person{
public:
Person(int a, int b)
{
this->m_A = a;
this->m_B = b;
}
int m_A;
int m_B;
};
//全局函数实现 + 号运算符重载
Person operator+(const Person & p1, const Person & p2)
{
Person temp;
temp.m_A = p1.m_A + p2.m_A;
temp.m_B = p1.m_B + p2.m_B;
return temp;
}
void test02()
{
Person p1(10, 10);
Person p2(10, 10);
Person p3 = p1 + p2; //相当于 operator+(p1, p2)
//结果为 p3.m_A = 20;
// p3.m_B = 20;
}
输入/输出运算符重载
在讲这部分内容之前,我们先来明确一下 cout
和 cin
的一些概念。
cout
是ostream
类的对象
cin
是istream
类的对象
在我们平时使用 cin
、 cout
时,并不能输入和输出我们自定义的类,我们可通过重载输入/输出运算符解决。
值得一提的是,我们在重载输入/输出运算符时,最好使用全局函数进行重载。
//输出运算符的重载
class Person{
public:
Person(int a, int b)
{
this->m_A = a;
this->m_B = b;
}
int m_A;
int m_B;
};
ostream & operator<<(const ostream & cout, Person & p)
{
cout << p.m_A << " " << p.m_B << endl;
return cout;
}
void test03()
{
Person p(10, 10);
cout << p << endl; //结果为10 10
}
输入运算符的重载同上,只不过要将 ostream
换成 istream
, <<
换成 >>
。
可重载运算符/不可重载运算符
下面是可重载的运算符列表:
类型 | 举例 |
---|---|
双目算术运算符 | + (加),-(减),*(乘),/(除),% (取模) |
关系运算符 | ==(等于),!= (不等于),< (小于),> (大于>,<=(小于等于),>=(大于等于) |
逻辑运算符 | ||(或), &&(与), !(非) |
单目运算符 | + (正),-(负),*(指针),&(取地址) |
自增自减运算符 | ++(自增),–(自减) |
位运算符 | (按位或),& (按位与),~(按位取反),^(按位异或),,<< (左移),>>(右移) |
赋值运算符 | =, +=, -=, *=, /= , % = , &=, |
空间申请与释放 | new, delete, new[ ] , delete[] |
其他运算符 | ()(函数调用),->(成员访问),,(逗号),[](下标) |
下面是不可重载的运算符列表:
.
:成员访问运算符.*
,->*
:成员指针访问运算符::
:域运算符sizeof
:长度运算符?:
:条件运算符#
: 预处理符号