C++学习课件(二)————面向对象
一、类和对象
1. 类
类是构成对象的一个蓝图 , 图纸
可以拥于属性 | 变量、(用于表示数据)
可以拥有方法 | 行为动作 ( 用于表示行为、动作 )
可以隐藏数据和方法
可以对外提供公开的接口
汽车的设计图纸 : 类
汽车: 对象。
python 弱语言
c++ 强类型语言
class stu():
init
self.name = "";
def run(self):
#include<string>
using namespace std;
class Student{
private:
string name; // 姓名
int age; //年龄
public:
void read(){
std::cout << "学生在看书" << std::endl;
}
};
2. 对象
1. 在栈中创建对象
用这种方法创建的对象,内存分配到栈里(Stack)。 直接使用
.
来访问成员。当程序对象所在的代码块结束运行(方法运行结束),对象会自动删除,内存自动释放。
class student{
...
}
int main(){
//对象存储于栈内存中
student stu1 ;
student stu2 ;
return 0 ;
}
2. 在堆中创建对象
这种方法创建的对象,内存分配到堆里(Heap)。一般使用
*
或者->
调用对象的方法。箭头操作符”->"将解引用(dereferencing*)和成员使用(member access.)结合起来,
#include<iostream>
#include<string>
using namespace std;
class student{
public:
void run(){
cout << "学生在跑步" << endl;
}
}
int main(){
//对象存储于栈内存中 new 关键字是在堆中申请动态内存,所以这里不能写成 s3 而应该是指针。
Student *s3 = new Student;
s3->run();
*s3.run();
return 0 ;
}
3. 访问修饰符
python中修饰成员访问权限,采用的是下划线的形态来修饰。
c++ 对于成员的访问有三种访问操作符
public
|prive
|protected
, 默认情况下是private
public
: 公开权限,任何位置都可以访问private
: 私有权限,只能自己内部访问及其友元函数protected
: 类内、子类及友元函数可访问
#include<iostream>
#include<string>
using namespace std;
class Student{
private: //表示下面的成员为私有
string name; // 姓名
int age; //年龄
public: //表示下面的函数为公开
void run(){}
};
ing main(){
Student stu ;
stu.name = "张三" ; // 禁止访问
stu.run(); //允许访问
return 0 ;
}
4. 实现类的成员函数 【 重点 】
1. 类中实现 或 外部实现
- 成员函数可以在类中声明时直接实现,也可以在类的外部。
- 可以在类的外部实现成员你函数 , 需要使用 类名::函数名
#include <iostream>
#include <string>
using namespace std;
class Student{
private :
int age ;
string name;
public :
void read(string bookname){
cout<< bookname << endl;
}
void speak(string something);
}
void Student::speak(string something){
cout << "说说说---" << something << endl;
}
int main(){
Student stu;
stu.read("哈利波特");
stu.speak("我喜欢看哈利波特");
return 0 ;
}
2. 分离声明和实现
声明放到 头文件中,实现放到cpp文件中 。 头文件的声明,需要在前后包裹一段特殊的样式代码。这段代码确保了一旦该头文件多次包含执行时,出现重复定义的错误。
如下所示: 当第一次包含
Student.h
时,由于没有定义_STUDENT_H_
,条件为真,这样就会包含(执行)#ifndef _STUDENT_H_
和#endif
之间的代码,当第二次包含Student.h
时前面一次已经定义了_STUDENT_H_
,条件为假,#ifndef_STUDENT_H_
和#endif
之间的代码也就不会再次被包含,这样就避免了重定义了。
- Student.h
//后面的大写表示标识,可以随意命名.
#ifndef HELLOWORLD_STUDENT_H
#define HELLOWORLD_STUDENT_H
#include <string>
using namespace std;
class Student{
private :
int age ;
string name;
public :
void read(string bookname);
void speak(string something);
};
#endif //HELLOWORLD2_STUDENT_H
- Student.cpp
#include "student.h"
#include <iostream>
using namespace std;
void Student::read(string bookname){
cout << "看书:"<<bookname<<endl;
}
void Student::speak(string something){
cout << "说说说---" << something << endl;
}
- main.cpp
#include <iostream>
#include "student.h"
int main() {
Student s;
s.read("哈利波特");
s.speak("hi harry");
return 0;
}
- CMakeList.txt
cmake_minimum_required(VERSION 3.14)
project(HelloWorld2)
set(CMAKE_CXX_STANDARD 14)
# 需要在后面添加student.cpp 因为main.cpp 依赖该文件
add_executable(HelloWorld main.cpp student.cpp)
打卡作业
- 定义学生类 stu , 包含
- 属性: 姓名(name) , 学号(no) , 分数(使用vector存储)
- 函数:putScore() 用于添加分数,接收int类型分数。
- 函数:updateScore() 用于更新该学生分数不足60分分数为60分。
- 函数:showInfo() 用于输出学生姓名、学号、总分、平均分、各个学科分数。
- 学生类应该具有stu.h 和 stu.cpp 两个文件
- stu.h 用于声明属性以及以上三个函数的声明
- stu.cpp 应该作为这三个函数的具体实现。
- 在main函数里面,定义局部变量 vector<stu *> stu_vector
- 用于存储学生信息,并且要求存储的类型为学生指针类型
- 该容器需要向下传递给initStu 、updateStu、printStu三个函数
- 定义score.h 和 score.cpp 用于完成学生信息以及分数的初始化、更新、打印功能。
- score.h : 用于声明 initStu 、updateStu、printStu 三个函数。
- score.cpp:应该作为上面三个函数的具体实现。
- 程序运行结束,需要完成内存回收释放工作。
二、特殊成员函数
当定义一个类后,它的对象在未来的操作中,总会不可避免的总会碰到如下的行为:
创建
、拷贝
、赋值
、移动
、销毁
。 这些操作实际上是通过六种特殊的成员函数来控制的:构造函数
、析构函数
拷贝构造函数
、拷贝赋值函数
、移动构造函数
、移动赋值函数
。默认情况下,编译器会为新创建的类添加这些函数,以便它的对象在未来能够执行这些操作。
1. 构造函数
1. 一般方式构造
构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。与类名同名,没有返回值,可以被重载,通常用来做初始化工作。 在
python
中,有类似的____init___
函数用于初始化工作
#include<string>
using namespace std;
class student{
string name;
int age ;
public :
//构造函数
student(){
cout << "执行无参构造函数" << endl;
}
student(string name ){
cout << "执行含有一个参数的构造函数" << endl;
}
student(string name , int age ){
cout << "执行含有两个参数的构造函数" << endl;
}
};
int main(){
//创建三个对象,会执行三个对应你的构造函数
student s1 ;
student s1{"张三"};
student s1{"张三",28};
return 0 ;
}
2. 初始化列表方式
在之前成员的初始化工作,都是在构造函数的函数体里面完成的。如果使用初始化列表,那么成员的初始化赋值是在函数体执行前完成,并且初始化列表有一个优点是:
防止类型收窄
,换句话说就是精度丢失
有三种情况需要使用到构造函数初始化列表
- 情况一、需要初始化的数据成员是对象的情况(这里包含了继承情况下,通过显示调用父类的构造函数对父类数据成员进行初始化);
- 情况二、需要初始化const修饰的类成员或初始化引用成员数据;
- 情况三、子类初始化父类的私有成员;
#include <iostream>
#include <string>
using namespace std;
class student{
string name;
int age;
/*
//早期的方式
student(string name_val , int age_val){
name = name_val;
age = age_val;
}
*/
//更好的方式
student(string name ,int age):name{name},age{age}{
cout << "执行有参构造函数" <<endl;
}
};
int main(){
//编译允许通过,输出 a1 和 a2 为 30 和20 ,小数点省略
int a (30.22);
int a = 20.33;
//编译失败,不允许赋值。防止类型收窄看精度丢失。
//int a{20.33};
student s("张三" , 18);
return 0 ;
}
3. 委托构造函数
一般来说,如果给类提供了多个构造函数,那么代码的重复性会比较高,有些构造函数可能需要包含其他构造函数中已有的代码,为了让编码工作更简单,C++11 允许程序员在一个构造函数的定义中使用另一个构造函数。这种现象被称为
委托
.
1. 早前的构造函数写法
早期的做法是,每个构造函数完成变量的赋值工作。
#include <iostream>
#include <string>
using namespace std;
class Student{
string name;
int age ;
public:
//无参构造
Student():name{"张三"},age{19}{
};
//一个参数构造
Student(string name_val):name{name_val},age{19}{
};
//两个参数
Student(string name_val , int age_val ):name{name_val},age{age_val}{
};
};
int main(){
Student s1;
Student s2("李四");
Student s3("李四" , 20 );
return 0 ;
}
2. 委托构造函数写法:
委托构造函数实际上就是已经有一个构造函数定义好了所有初始化工作的逻辑,那么剩下的构造函数就不用做这个活了,全部交给它来做即可。有点类似,A调用C, B调用C ,而C 把所有的初始化代码都写完了。那么A和B只是负责委托C来完成即可
#include <iostream>
#include <string>
using namespace std;
class Student{
string name;
int age ;
public:
//无参构造 委托给两个参数的构造函数
Student():Student{"张三" , 19}{
};
//一个参数构造 委托给两个参数的构造函数
Student(string name_val):Student{name_val , 19}{
};
//由该函数完成最终的成员赋值工作
Student(string name_val , int age_val):name{name_val},age{age_val}{
};
};
int main(){
Student s1;
Student s2("李四");
Student s3("李四" , 20 );
return 0;
}
2. 析构函数
和python一样,c++也有析构函数。类的析构函数是类的一种特殊的成员函数,与构造函数正好相反,它会在每次删除所创建的对象时执行。
析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。不能被重载,一般用于释放资源。
#include <iostream>
#include <string>
using namespace std;
class Student{
string name;
int age ;
public :
//构造函数
Student(){
cout << "执行无参构造函数" <<endl;
}
Student(string name ){
cout << "执行含有一个参数的参构造函数" <<endl;
}
Student(string name , int age ){
cout << "执行含有两个参数的构造函数" <<endl;
}
//析构函{}
~Student(){
cout << "执行析构函数" <<endl;
}
};
int main(){
Student *s1 = new Student();
Student *s2 = new Student();
Student *s3 = new Student();
//释放对象
delete s1;
delete s2;
delete s3;
return 0 ;
}
3. 拷贝构造函数
1. 初探拷贝
C++中经常使用一个常量或变量初始化另一个变量, 比如:
int mian(){
int a = 3;
int b = a;
return 0 ;
}
使用类创建对象时,构造函数被自动调用以完成对象的初始化,那么能否象简单变量的初始化一样,直接用一个对象来初始化另一个对象呢?
不难看出,s2对象中的成员数据和 s1 是一样的。相当于将s1中每个数据成员的值复制到s2中,这是表面现象。实际上,系统调用了一个拷贝构造函数。
#include <iostream>
#include <string>
using namespace std;
class student{
public :
string name;
int age ;
student(string name , int age ){
cout << "执行含有两个参数的构造函数" << endl;
}
~student(){
cout << "执行析构函数" <<endl;
}
};
int main(){
Student s1{"张三" , 19 };
cout << s1.name << " : " << s1.age <<ednl;
Student s2 = s1;
cout << s2.name << " :: " << s2.age <<ednl;
return 0 ;
}
2. 浅拷贝
指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。如果数据中有属于
动态成员
( 在堆内存存放 ) ,那么浅拷贝只是做指向而已,不会开辟新的空间。默认情况下,编译器提供的拷贝操作即是浅拷贝。
include <iostream>
include <string>
using namespace std;
class Student {
public:
int age ;
string name;
public :
//构造函数
Student(string name , int age ):name(name),age(age){
cout<< " 调用了 构造函数" << endl;
}
//拷贝构造函数
Student(const Student & s){
cout << "调用了拷贝构造函数" << endl;
age = s.age;
name = s.name;
}
//析构函数
~Student(){
cout << "调用了析构函数" << endl;
}
};
int main(){
Student s1("张三" , 18);
cout << s1.name << " : " << s1.age <<endl;
Student s2 = s1;
cout << s2.name << " :: " << s2.age <<endl;
return 0 ;
}
3. 浅拷贝引发的问题
默认情况下,浅拷贝已经足以应付日常的需求了,但是当类中的成员存在动态成员(指针)时,浅拷贝往往会出现一些奇怪的问题。
#include <iostream>
#include <string>
using namespace std;
class Student {
public:
string name;
string *address;
Student(string name , string * address):name(name),address(address){
cout << "执行构造函数" << endl;
}
// 这里还是默认的浅拷贝。 由于address是指针类型,如果是浅拷贝,那么两个指针会指向同一个位置。
Student(const Student & s){
cout << "调用了拷贝构造函数" << endl;
name = s.name;
address = s.address;
}
//析构函数
~Student(){
cout << "调用了析构函数" << endl;
//这里将会删除两次内存空间
delete address;
address = nullptr;
}
};
int main(){
string address="深圳";
Student s1("张三" , &address);
//此处会执行拷贝。
Student s2 = s1;
cout << s2.name << " : " << s2.address << endl;
//修改第一个学生的地址为:北京
*s1.address = "北京"
//第二个学生的地址也会变成北京
cout << s2.name << " : " << s2.address << endl;
return 0 ;
}
4. 深拷贝
深拷贝
也是执行拷贝,只是在面对对象含有动态成员
时,会执行新内存的开辟,而不是作简单的指向。在发生对象拷贝的情况下,如果对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配。
#include <iostream>
#include <string>
using namespace std;
class Student {
public:
string name;
string *address;
Student(string name , string * address):name(name),address(address){
cout << "执行构造函数" << endl;
}
//深拷贝
Student(const Student & s){
cout << "调用了拷贝构造函数" << endl;
age = s.age;
name = s.name;
if(address == nullptr){
//开辟新的空间
address = new string();
*address = s.address;
}
}
//析构函数
~Student(){
cout << "调用了析构函数" << endl;
if(address != nullptr){
delete address;
address = nullptr;
}
}
};
int main(){
string address="深圳";
Student s1("张三" , &address);
//此处会执行拷贝。
Student s2 = s1;
cout << s2.name << " : " << s2.address << endl;
//修改第一个学生的地址为:北京
*s1.address = "北京"
//第二个学生的地址也会变成北京
cout << s2.name << " : " << s2.address << endl;
return 0 ;
}
5. 触发拷贝的场景
如果是生成临时性对象或者是使用原有对象来初始化现在的对象,那么都会执行拷贝构造。一般调用拷贝构造函数的场景有以下几个:
对象的创建依赖于其他对象。
函数参数(传递对象)
函数返回值 (返回对象)
Student.cpp
#include <iostream>
#include <string>
using namespace std;
class Student {
public:
int age ;
string name;
public :
//构造函数
Student(){
age = 19 ;
name = "张三";
cout<< " 调用了 构造函数" << endl;
}
//拷贝构造函数
Student(const Student & s){
cout << "调用了拷贝构造函数" << endl;
age = s.age;
name = s.name;
}
//析构函数
~Student(){
cout << "调用了析构函数" << endl;
}
};
1. 对象创建依赖于其他对象
#include <iostream>
#include <string>
#include "Student.cpp"
using namespace std;
int main(){
Student stu1("张三",18); //执行构造函数
Student stu2 = stu1; //执行拷贝构造函数
return 0 ;
}
2. 函数参数
编译器不会优化这个方向,因为一旦优化掉了,那么就不会执行拷贝的工作,那就代表传递进来的对象实际上就是外部的原对象,有可能在函数内部对对象进行了修改,会导致外部对象跟着修改。所以默认情况下,只要是函数参数传递,都会发生拷贝。 如果不想发生拷贝,请使用 引用 或者 指针。
#include <iostream>
#include <string>
#include "Student.cpp"
using namespace std;
void printStun(Student s){
cout << s.name << " : "<< s.age << endl;
}
int main(){
Student stu1("张三",18); //执行构造函数
printStudent(stu1); //执行拷贝构造函数
return 0;
}
3. 函数返回值
为了避免过多的临时性对象创建,消耗内存,编译器内部做了优化,函数的返回值不会再产生临时对象,直接把生成的对象赋值给外面接收的变量
#include <iostream>
#include <string>
#include "Student.cpp"
using namespace std;
//构造函数 由于函数执行完毕会有个临时对象来接收值,所以这里还会执行拷贝构造
Student createStu(){
return Student("张三",18);
}
int main(){
Student stu = createStu(); //这里还会执行构造函数
return 0;
}
4. 编译器自动优化
编译器有时候为了避免拷贝生成临时对象而消耗内存空间,所以默认会有优化、避免发生过多的拷贝动作所以打印的日志可能不是我们所期望的,这时候,如果手动编译的话,可以添加参数,
#如果手动编译 可以添加以下参数 -fno-elide-constructors
g++ -std=c++11 main.cpp -fno-elide-constructors
# 如果使用cmake编译,可以添加配置
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-elide-constructors")`
6. 再说函数返回值
c++ 严禁函数返回内部的存放于栈内存的局部变量引用或者指针,因为函数一旦执行完毕,那么内部的所有空间都将会被释放,这时如果在外面操作返回值,将出现错误。所以有时候临时拷贝的工作就变得不可避免。
int* getNum(){
int a = 9;
return &a ;
}
int* getNum2(){
int a = new int(9);
return &a ;
}
int main(){
int *p = getNum(); //错误! 一旦getNum()执行完毕,内部栈空间都将释放掉。
int *p = getNum2(); // 正确,因为函数返回的指针指向的地址位于堆内存中。
return 0 ;
}
7. 移动构造函数
有时候我们需要新创建的对象,拥有旧对象一模一样的数据,当然这可以使用拷贝操作来完成。但是假设旧对象不再使用,那么拷贝操作就显得有点弱,因为旧对象仍然占据着空间。C++11中推出了 移动构造操作,也就是完全的把旧对象的数据 “移动” 到新对象里面去,并且把旧对象被清空了。
注意: 移动构造或者是拷贝构造,针对的都是数据的拷贝或者移动,对象的该创建还是创建,他们两针对的仅仅是数据是以和种方式得来而已。
- 移动语义
要想完成移动构造,需要配合右值引用来实现。因为把某一份数据,移动到另一个地方去,实际上操作的是右值,而右值引用恰好指向的是右值。这常常出现在对象的搬运上,假设现在要做两份数据的交换,最初的设想是进行值的拷贝、复制,但是这样会产生一份临时拷贝值,不如直接移动过去划算。但在C++里面的
移动
实际上并不是真的在移动数据,只是窃取了原来数据的控制权而已。
1. 没有移动构造
使用getStu函数生成一个学生对象,调用返回后,使用stu来接收。 在没有移动构造函数的情况下,临时对象的产生会调用拷贝构造。此举会造成资源的浪费,并且效率也低。编译器为了避免过多的临时对象的创建工作,内部已经做了优化。
- 学生类
class Student{
public :
int *age;
Student():age(new int(18)){
cout << "执行构造函数~!~"<<endl;
}
//深拷贝
Student(const Student &s):age(new int(*s.age)){
cout << "执行拷贝构造函数~!~"<<endl;
}
//移动赋值
~Student(){
cout <<"执行析构函数~!" << endl;
delete age;
}
};
Student getStu(){
Student s ;
cout <<"getTemp =" << __func__ << " : " << hex << s.age << endl;
return s;
}
- main函数
int main(){
Student stu = getStu();
return 0 ;
}
2. 使用移动构造
使用移动构造,会使得在返回对象时,不会调用拷贝构造函数,这也就避免产生了对象的拷贝工作。
- 学生类
class Student{
public :
int *age;
Student():age(new int(18)){
cout << "执行构造函数~!~"<<endl;
}
//深拷贝
Student(const Student &s):age(new int(*s.age)){
cout << "执行拷贝构造函数~!~"<<endl;
}
//移动构造
Student( Student &&s):age(s.age){
cout << "执行移动!!!构造函数~!~"<<endl;
//移动之后,一般即可让原有对象的指针变成空指针
s.age = nullptr;
}
//移动赋值
~Student(){
cout <<"执行析构函数~!" << endl;
delete age;
}
};
Student getStu(){
Student s ;
cout <<"getTemp =" << __func__ << " : " << hex << s.age << endl;
return s;
}
- main
int main(){
Student stu = getStu();
return 0 ;
}
3. std::move函数
move
函数名字很具有迷惑性,但是它并不能移动任何东西。它唯一的作用就是把一个左值转化成一个对应的右值引用类型,继而可以通过右值引用使用该值。并且在使用move函数可以避免不必要的拷贝工作,move
是将对象的状态或者所有权从一个对象转化到另一个对象,只是转换状态或者控制权,没有内存的搬迁和拷贝 . 使用move函数即表示该对象已经不再使用,要被即将销毁了。
int main(){
int a = 3;
int &b = a ; //左值引用指向左值
int &&c = 3; //右值引用指向右值
int &&d = move(b); //右值引用,指向右值, move函数强制转化 左值引用b 成右值引用
return 0 ;
}
- 使用move出发移动构造函数
默认情况下,如果直接赋值,那么执行的是拷贝构造函数,并且有时候为了避免频繁的执行拷贝工作,可以直接使用
move
函数转化左值成右值引用,进而变成直接操作数据。
int main(){
Student stu1 ;
Student stu2 = stu1; // 执行拷贝构造函数
Student stu3 = move(stu1); // 执行移动构造函数
Student stu3(movestu1) ; // 和上一行代码同效果
return 0 ;
}
三、其他细节
1. this 指针
1. 引入
在早前的编码中,我们都会刻意的避免的函数的参数名称和成员变量名称一致,这是为了让我们更方便的赋值。其实他们的名称也可以一样,只是要多费点功夫而已。如下:
#include <iostream>
#include <string>
class Student{
int age ;
string name;
Student(int age_val , string name_val){
//默认情况下,前面的两个变量有编译器加上的this指针
age = age_val ;
name = name_val ;
}
Student(int age , string name){
//默认情况下,下面两行代码并没有完成赋值工作。
age = age ;
name = name ;
//=============================
//前面加上this -> 即可完成操作。
this-> age = age ;
this-> name = name ;
}
};
2. 含义
this
只能在成员函数中使用。全局函数,静态函数都不能使用this
。全局函数不属于任何一个类,静态函数也不依赖任何一个类,所以他们都不会有this指针
。 在外部看来,每一个对象都拥有自己的成员函数,一般情况下不写this,而是让系统进行默认配置。this指针永远指向当前对象 , 所以成员函数中的,通过this即可知道操作的是哪个对象的数据。this指针的作用有两个
- 处理同名变量问题
- 返回对象本身
#include <iostream>
#include <string>
class Student{
int age ;
string name;
Student(int age , string name){
//默认情况下,下面两行代码并没有完成赋值工作。
age = age ;
name = name ;
//=============================
//前面加上this -> 即可完成操作。
this-> age = age ;
this-> name = name ;
}
//获取对象本身。注意: 这里的返回值不要写成Student,否则返回的是一个临时的拷贝对象
Student& getStu(){
return *this; //this表示指针, 在指针前面加上* 表示解引用。根据指针获取到指向的对象
}
};
int main(){
Student s1 (18 , "张三");
Student &s2 = s1.getStu(); //使用引用来接收,表示指向同一个引用,
return 0 ;
}
2. 常函数
在编写代码的时候,如果确定了某个成员函数不会修改成员变量,那么可以把该函数声明为常函数。常函数其实也是函数,它浓缩了函数和
常
这个概念在里面,函数体内不允许修改成员变量,除非该变量使用mutable
修饰.
常函数的const修饰的是this指针,在常函数中的this形同:
const 类名
, 表示指向常量的指针。 所以也就解释了为什么在常函数中无法修改成员变量。
#include<iostream>
#include<string>
using namespace std;
class Student{
public:
string name = "无名氏";
int age = 18;
public :
//在此添加 const 即可变成常函数,
void printStudent() const{
// age = 10 ; 错误,不允许修改
cout << name << " " << age << endl;
}
};
3. 常对象
常对象其实也就是一个变量,和以前的常量没有相差多少。不同的是如今对象里面可以能包含很多的变量,以前的常量指的只是一个变量而已。 若类中有变量使用
mutable
修饰,那么该变量在常对象状态下,可以被修改
#include<iostream>
#include<string>
using namespace std;
class Student{
public:
string name = "无名氏";
int age = 18;
public :
//在此添加 const 即可变成常函数,
void printStudent() const{
cout << name << " " << age << endl;
}
void run(){
cout<< age << "的" <<name <<"在跑步" <<endl;
}
};
int main(){
const Student s1 ("zhangsan " ,18);
cout << s1.name << endl; //访问常对象中的成员 ,OK
s1.name = "李四" ; // 试图修改常对象中的成员 , error
return 0 ;
}
- 注意:
-
- 无法修改常对象中的成员(变量)
- 除非某个成员使用
mutable
修饰 - 常函数函不能访问普通的成员函数,但是可以访问常函数
- 普通的对象可以访问成原函数,也可以访问常函数
-
4. 静态成员
有时候需要不管创建多少次对象,某些变量的值都是一样的,那么此时可以把该变量变成静态修饰(static).
静态成员被类的所有对象所共享,成员不能在类中定义的时候初始化,但是可以在类的外部通过使用范围解析运算符 :: 来重新声明静态变量从而对它进行初始化
1. 静态成员变量
假设现在要记录来参加当年毕业的学生信息,无论他们的身份迥异,都有一个共有的属性,就是同一个学校。如果不对该属性进行修饰,那么100个学生对象就要开辟100个位置来存储同一个school数据,但是其实可以使用static修饰,只需要一份空间即可。
#include<iostream>
#include<string>
using namespace std;
class Student{
public:
int age ;
string name;
static string school; //静态成员
};
Student::school = "北京大学";
int main(){
Student s;
s.name = "张三";
s.age = 18 ;
cout <<s.school << " " << s.name <<" " << s.age << endl;
return 0 ;
}
2. 静态成员函数
如果把函数成员声明为静态的,就可以把函数与类的任何特定对象独立开来。静态成员函数即使在类对象不存在的情况下也能被调用,静态函数只要使用类名加范围解析运算符 :: 就可以访问。
静态成员函数只能访问静态成员数据、其他静态成员函数和类外部的其他函数。
静态成员函数有一个类范围,他们不能访问类的 this 指针。您可以使用静态成员函数来判断类的某些对象是否已被创建。
#include<iostream>
#include<string>
using namespace std;
class Student{
public:
int age ;
string name;
static string school; //静态成员
public :
static string getSchool(){
//不能访问 name 和 age
return school;
}
};
Student::school = "北京大学";
int main(){
Student s ;
cout << s.getSchool() << endl; //可以访问
//也可以使用类名的方式访问
cout << Student::school <<endl;
return 0 ;
}
- 注意:
- 静态成员属于类,不属于对象
- 静态成员变量必须在类中声明,在类的外部初始化
- 静态成员变量的声明周期是整个程序,作用域在类的内部
- 静态成员函数只能访问静态成员变量
- 静态成员可以使用类来访问,也可以使用对象来访问。
5. 结构体和类
结构体是一个由程序员定义的数据类型,可以容纳许多不同的数据值。在过去,面向对象编程的应用尚未普及之前,程序员通常使用这些从逻辑上连接在一起的数据组合到一个单元中。一旦结构体类型被声明并且其数据成员被标识,即可创建该类型的多个变量,就像可以为同一个类创建多个对象一样。 结构体是由C语言发明出来的,它和类其实没有差多少,只是它的所有成员默认都是public公开。
声明结构体的方式和声明类的方式大致相同,其区别如下:
- 使用关键字 struct 而不是关键字 class。
- 尽管结构体可以包含成员函数,但它们很少这样做。所以,通常情况下结构体声明只会声明成员变量。
- 结构体声明通常不包括 public 或 private 的访问修饰符, 因为它的所有成员默认都是public
- 类成员默认情况是私有的,而结构体的成员则默认为 public。程序员通常希望它们保持公开,只需使用默认值即可。
#include<iostream>
#include<string>
struct Student{
string name;
int age;
void run(){
}
}
int main(){
Student s1 ;
s1.name = "zhangsan";
s1.age = 18 ;
return 0 ;
}
6. 友元
- 概念理解
私有成员只能在类的成员函数内部访问,如果想在别处访问对象的私有成员,只能通过类提供的接口(成员函数)间接地进行。这固然能够带来数据隐藏的好处,利于将来程序的扩充,但也会增加程序书写的麻烦。
在类的外部无法访问类的私有成员,但是有时候需要它,那么可以借助友元函数 来实现。
友元函数
是一种特权函数,C++允许它访问私有成员。程序员可以把一个全局函数、成员函数、甚至整个类声明为友元
友元可以看成是现实生活中的 好闺蜜 或者是 好基友
1. 友元函数
友元函数可以直接访问类中的私有成员
#include<iostream>
#include<string>
using namespace std;
class Car{
private:
string color {"红色"};
friend void showColor(Car c);
};
//实现友元函数,函数内部可以直接访问私有成员
void showColor(Car c) {
cout << c.color << endl;
}
int main(){
Car c ;
showColor(c);
return 0 ;
}
2. 友元类
一个类 A 可以将另一个类 B 声明为自己的友元,类 B 的所有成员函数就都可以访问类 A 对象的私有成员
#include<iostream>
#include<string>
using namespace std;
class Car{
private:
string color {"红色"};
friend class SSSS; //声明 4S 为友元类
public:
string getColor(){
return color;
}
};
class SSSS{
public:
void modifyCar( Car &myCar){ //改装汽车
myCar.color = "黑色";
}
};
int main(){
SSSS s;
Car c;
s.modifyCar(c);
cout << c.getColor() << endl;
return 0 ;
}
四、运算符重载
1. 什么是运算符重载
函数可以重载, 运算符也是可以重载的。 运算符重载是对已有的运算符重新进行定义,赋予其另一种功能,以达到适应不同的数据类型。运算符重载不能改变它本来的寓意(也就是 加法不能变更为 减法)
运算符重载只是一种 “语法上的方便” , 背后实际上是一种函数调用的方式。
#include<iostream>
using namespace std;
class Student{
int age;
public:
Student(int age):age(age){
}
};
int main(){
int a = 3 ;
int b = 4 ;
//两个整数相加,这是允许的。
int c = a + b ;
Student s1(10) ;
Student s2(20) ;
//两个学生相加,则编译失败。即使你认为两个学生的年纪之和为第三个学生的年纪
//依然不允许通过编译,因为 + 运算符在定义之初就不认识Student这种类型。
Student s3 = s1 + s2 ; // 编译器不通过
return 0 ;
}
2. 运算符重载的意义
运算符的重载实际上背后是调用对应的函数,重载运算符使得把复杂的代码包装起来,对外暴露简单的一个符号即可。实际上不使用运算符重载,也一样可以实现功能,如下面两个学生相加的示例所示:
#include<iostream>
using namespace std;
class Student{
public:
int age;
Student(int age):age(age){
}
};
int main(){
Student s1(10);
Student s2(20);
//先对两个学生的年龄做加法
int age = s1.age + s2.age;
//再赋值给第三名学生
Student s3 (age);
cout << "第三名学生的年龄是: " << s3.age << endl;
return 0 ;
}
3. 定义运算符
重载的运算符是带有特殊名称的函数,函数名是由关键字
operator
和其后要重载的运算符符号构成的。比如,要重载+
运算符 ,那么可以声明一个函数为operator+()
,函数声明的位置可以是类的内部,也可以是类的外部,所以又有了成员函数和全局函数的划分。与其他函数一样,重载运算符函数,也可以拥有返回值和参数列表。此处仍然以学生相加的案例举例。
1. 成员函数方式
把重载的运算符函数定义在类中,此时只需要接收一个参数,因为类的对象本身作为
+
的前面调用者。
#include<iostream>
using namespace std;
class Student{
public:
int age;
Student(int age):age(age){
}
s1 + s2;
//两个学生的年龄之和,则为第三个学生的命令,所以此处需要返回一个学生对象。
//好方便在外面接收。
Student operator+ (Student &s ){
Student temp(this->age + s.age);
return temp;
}
};
int main(){
Student s1(10);
Student s2(20);
//这里等于使用s1的对象,调用了operator+这个函数, +后面的 s2 则被当成参数来传递
Student s3 = s1 + s2;
cout << "s3.age = " << s3.age << endl;
return 0 ;
}
2. 全局函数方式
并不是所有的运算符重载都能定义在类中,比如,需要扩展一些已有的类,对它进行运算符重载,而这些类已经被打成了一个库来使用,此时通过全局函数的方式来实现运算符重载。
#include<iostream>
using namespace std;
class Student{
public:
int age;
Student(int age):age(age){
}
};
//由于函数并非定义在类中,所以此处无法使用到this指针,则需要传递两个对象进来。
Student operator+ (Student &s , Student &ss ){
Student temp(s.age + ss.age);
return temp;
}
int main() {
Student s1(20);
Student s2(30);
//这里等于使用s1的对象,调用了operator+这个函数, +后面的 s2 则被当成参数来传递
Student s3 = s1 + s2;
cout << "s3.age = " << s3.age << endl;
return 0;
}
练习
学生常常有自己的零用钱,一天张三和李四碰面,两人在合计计算他们的零用钱之和,请使用运算符重载的方式,设计他们的行为。
class stu{
int momey ;
4. 输出运算符重载
输出运算符重载,实际上就是
<<
的重载。<<
实际上是位移运算符,但是在c++里面,可以使用它来配合cout
进行做控制台打印输出。cout
其实是ostream
的一个实例,而ostrem
是 类basic_ostream
的一个别名,所以之所以能使用cout <<
来输出内容,全是因为basic_ostream
里面对<<
运算符进行了重载.
<<` 运算符只能输出常见的基本数据类型,对于自定义的类型,是无法识别的。比如: `student
#include <iostream>
using namespace std;
class Student{
public:
string name {"zhangsan"};
public:
Student(string name){
this->name = name;
}
};
//对 << 运算符进行重载。 这里使用的是全局函数 ,第一个参数ostream 是因为在外面调用<<这个运算符的时候,cout 在前,cout 为ostream这种类型的对象。
ostream& operator<< (ostream& o, Student &s1){
o << s1.name ;
return o;
}
int main() {
Student s1("张三");
cout << s1 <<endl ;
return 0;
}
5. 输入运算符重载
输入运算符重载,实际上就是
>>
的重载。用意和上面的输出运算符相似
#include <iostream>
using namespace std;
class Student{
public:
string name
//对 << 运算符进行重载。
ostream& operator<< (ostream& o, Student &s1){
o << s1.name ;
return o;
}
//对 >> 运算符进行重载
istream& operator>> (istream& in, Student &s1){
in >> s1.name;
return in;
}
int main() {
Student s1;
cout << "请输入学生的姓名:" << endl;
cin >> s1;
//打印学生, 实际上是打印他的名字。
cout << s1 <<endl ;
return 0;
}
练习
学生类中有姓名和年龄属性,请重载输入运算符,以便从键盘直接获取姓名和年龄。形如 :
stu s ;
cin >> s; //键盘录入 姓名 年龄 ( 中间有空格区分)
6.赋值运算符重载
1. 默认的赋值运算符
赋值运算符在此前编码就见过了,其实就是
=
操作符 。
class Student{
int no;
int age ;
public :
Student(no , age){
this->no = no ;
this->age = age ;
}
}
int main(){
Student s1(10001 , 15);
Student s2 ;
s2 = s1; //此处使用了默认的赋值运算符。
Student s2 = s1;//此处执行的是拷贝构造函数
return 0 ;
}
2. 拷贝赋值运算
其实这里说的就是
=
赋值运算符
#include<iostream>
using namespace std;
class Student{
int no;
int age ;
public :
Student(no , age){
this->no = no ;
this->age = age ;
}
//拷贝赋值
Stu& operator=(const Stu &h){
cout <<"执行拷贝赋值函数" << endl;
d = new int();
*d = *h.d;
}
}
int main(){
Stu stu1("张三",18);
Stu stu2 ;
stu2 = stu1; //拷贝赋值
return 0 ;
}
3. 移动赋值运算
移动赋值运算,接收的是一个右值,并且移动之后,原有对象将不再拥有对数据的控制权。
#include<iostream>
using namespace std;
class Student{
int no;
int age ;
public :
Student(no , age){
this->no = no ;
this->age = age ;
}
//拷贝赋值
Stu& operator=(const Stu &h){
cout <<"执行拷贝赋值函数" << endl;
d = new int();
*d = *h.d;
}
//移动赋值
Stu& operator=(const Stu &&h){
cout <<"执行移动赋值函数" << endl;
d = h.d;
h.d = nullptr;
return *this;
}
}
int main(){
Stu stu1("张三",18);
Stu stu2 ;
stu2 = move(stu1); //移动后,stu1 将不再拥有对数据的控制权
return 0 ;
}
7. 调用运算符重载
一般来说,可以使用对象来访问类中的成员函数,而对象本身是不能像函数一样被调用的,除非在类中重载了
调用运算符
。 如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象。在外面使用对象()
,实际上背后访问的是类中重载的调用运算符函数。如果某个类重载了调用运算符,那么该类的对象即可称之为:函数对象 ,因为可以调用这种对象,所以才说这些对象行为
像函数一样
。
#include<iostream>
using namespace std;
class Calc{
public:
int operator()(int val){
return val <0 ? -val :val;
}
};
int main(){
Calc c ;
int value = c(-10);
return 0;
}
- 标准库中的函数对象
在标准库中定义了一组算术运算符 、关系运算符、逻辑运算符的类,每个类都有自己重载的调用运算符。要想使用这些类,需要导入
#include
, 后面要说的lamdda表达式
正是一个函数对象
#include<functional>
int main(){
plus<int > p; //加法操作
int a = p(3 , 5);
negate<int> n; //可以区绝对值
cout <<n(-10) << endl;
return 0 ;
}
五、lambda表达式
1. lambda入门
也叫做
lambda 函数
, lambda 表达式的出现目的是为了提高编码效率,但是它的语法却显得有点复杂。lambda表达式表示一个可以执行的代码单元,可以理解为一个未命名的内联函数。
1. 表达式的语法
在编写lambda表达式的时候,可以忽略参数列表和返回值类型,但是前后的捕获列表和函数体必须包含 , 捕获列表的中括号不能省略,编译根据它来识别后面是否是
lambda表达式
,并且它还有一个作用是能够让lambda的函数体访问它所处作用域的成员。
//语法
[捕获列表](参数列表)->返回值类型{函数体}
2. 简单示例
- 示例1
//示例1:
[](int a ,int b)->int{return a + b ;} ; //一个简单的加法
- 示例2
如果编译器能推断出来类型。,那么 ->int 也可以省略掉
[](int a ,int b){return a + b ;} ;
- 示例3
如果不需要参数,那么参数列表页可以忽略。至此不能再精简了。
[]{return 3 + 5 ;};
- 示例4
这是最精简的lambda表达式了,不过没有任何用处,等于一个空函数,没有函数体代码
[]{};
2. lambda的使用
lambda表达式
定义出来并不会自己调用,需要手动调用。 如下面所示的,一个加法的案例
- 使用变量接收,然后再调用
#include <iostream>
using namespace std;
int main(){
//1. 接收lambda表达式,然后调用
auto f = [](int a ,int b)->int{return a + b ;};
//2. 调用lambda表达式
int result = f(3,4); //调用lambda函数,传递参数
cout << "result = " << result << endl;
return 0 ;
}
- 不接受表达式,直接调用
#include <iostream>
using namespace std;
int main(){
///2. 不接收,立即调用。 后面的小括号等同于调用这个函数。
int result= [](int a ,int b){return a + b }(3,4);
cout << "result = " << result << endl;
return 0 ;
}
3. 捕获列表的使用
labmda表达式
需要在函数体中定义,这时如果想访问所处函数中的某个成员,那么就需要使用捕获列表了。捕获列表的写法通常有以下几种形式:
形式 | 作用 |
---|---|
[a] | 表示值传递方式捕获变量 a |
[=] | 表示值传递方式捕获所有父作用域的变量(包括this) |
[&a] | 表示引用方式传递捕获变量a |
[&] | 表示引用传递方式捕获所有父作用域的变量(包括this) |
[this] | 表示值传递方式捕获当前的this指针 |
[=,&a,&b] | 引用方式捕获 a 和 b , 值传递方式捕获其他所有变量 (这是组合写法) |
#include<iostream>
using namespace std;
int main(){
int a = 3 ;
int b = 5;
//值传递方式捕获 a 和 b , 在lanmbda 表示内部无法修改a和b的值
auto f1 = [a,b]{return a + b;};
cout << f1() << endl; //打印 8
//引用方式捕获 a 和 b , 可以在内部修改a 和 b的值
auto f2 = [&a,&b]{
a = 30; //这里修改会导致外部的a 也跟着修改。
return a + b;
};
cout << f2() << endl; //这里打印35
cout << "a= "<< a << endl; //再打印一次,a 变成30了
return 0 ;
}
4. lambda 的应用场景
编写lamdda表达式很简单,但是用得上lambda表达式的地方比较特殊。一般会使用它来封装一些逻辑代码,使其不仅具有函数的包装性,也具有可见的自说明性。
在C++ 中,函数的内部不允许在定义函数,如果函数中需要使用到某一个函数帮助计算并返回结果,代码又不是很多,那么lambda表达式不失为一种上佳选择。如果没有lambda表达式,那么必须在外部定义一个内联函数。 来回查看代码稍显拖沓,定义lambda函数,距离近些,编码效率高些。
lambda表达式其实就是一个内联函数。
1. 没有使用lambda函数
计算6科考试总成绩。
#include<iostream>
using namespace std;
int getCout(vector<int> scores){
int result = 0 ;
for(int s : scores){
result += s;
}
return result;
}
int main(){
vector<int> scores{80,90,75,99,73,23};
//获取总成绩
int result = getCout(scores);
cout <<"总成绩是: "<< result << endl;
return 0 ;
}
2. 使用labmda表达式
lambda函数属于内联,并且靠的更近,也便于阅读。
#include<iostream>
using namespace std;
int main(){
vector<int> scores{80,90,75,99,73,23};
int result2 = [&]{
int result = 0 ;
for(int s : scores){
result += s;
}
return result;
}();
cout <<"总成绩是2: "<< result2 << endl;
return 0 ;
}
练习:
现在要比较两个学生的大小,可以按照名字多少排序比较、也可以按照年龄比较、也可以按照身高比较。定义一个全局函数compare , 接收三个参数,参数一和参数二 为要比较的学生对象,参数三为比较逻辑的函数(此应该使用lambda表达式来写,根据lambda表达式的返回值来决定谁大谁小。)
六 、 继承
1. 什么是继承
继承是类与类之间的关系,是一个很简单很直观的概念,与现实世界中的继承类似,例如儿子继承父亲的财产。
继承(Inheritance)可以理解为一个类从另一个类获取成员变量和成员函数的过程。例如B类 继承于A类,那么 B 就拥有 A 的成员变量和成员函数。被继承的类称为父类或基类,继承的类称为子类或派生类。 子类除了拥有父类的功能之外,还可以定义自己的新成员,以达到扩展的目的。
1. is A 和 has A
大千世界,不是什么东西都能产生继承关系。只有存在某种联系,才能表示有继承关系。如:哺乳动物是动物,狗是哺乳动物,因此,狗是动物,等等。所以在学习继承后面的内容之前,先说说两个术语
is A
和has A
。
- is A
是一种继承关系,指的是类的父子继承关系。表达的是一种方式:这个东西是那个东西的一种。例如:长方体与正方体之间–正方体是长方体的一种。正方体继承了长方体的属性,长方体是父类,正方体是子类。
- has A
has-a 是一种组合关系,是关联关系的一种(一个类中有另一个类型的实例),是整体和部分之间的关系(比如汽车和轮胎之间),并且代表的整体对象负责构建和销毁部分对象,代表部分的对象不能共享。
2. 继承入门
通常在继承体系下,会把共性的成员,放到父类来定义,子类只需要定义自己特有的东西即可。或者父类提供的行为如不能满足,那么子类可以选择自定重新定义。
#include <iostream>
#include <string>
using namespace std;
//父类
class Person{
public:
string name;
int age ;
};
//子类
class Student:public Person{
};
int main(){
//子类虽然没有声明name 和 age ,但是继承了person类,等同于自己定义的效果一样
Student s;
s.name = "张三";
s.age = 18;
cout << s.name << " = " << s.age << endl;
return 0 ;
}
3. 访问权限回顾
当一个类派生自基类,该基类可以被继承为 public、protected 或 private 几种类型。继承类型是在继承父类时指定的。 如:
class Student : public Person
。 我们几乎不使用 protected 或 private 继承,通常使用 public继承。
- public
表示公有成员,该成员不仅可以在类内可以被访问,在类外也是可以被访问的,是类对外提供的可访问接口;
- private
表示私有成员,该成员仅在类内可以被访问,在类的外面无法访问;
- protected
表示保护成员,保护成员在类的外面同样是隐藏状态,无法访问。但是可以在子类中访问。
1. 公有继承(public)
基类成员在派生类中的访问权限保持不变,也就是说,基类中的成员访问权限,在派生类中仍然保持原来的访问权限;
#include <string>
using namespace std;
class person{
public:
string name;
private:
int age;
};
class student:public person{
//name 和 age保持原有访问权限。
};
int main(){
student s;
s.name = "张三" ;
s.age = 18 ; //编译错误! 无法访问 age
return 0 ;
}
2. 私有继承(private)
基类所有成员在派生类中的访问权限都会变为私有(private)权限;
#include <string>
using namespace std;
class person{
public:
string name;
private:
int age;
};
class student:private person{
//name 和 age保持全部变成private权限
};
int main(){
student s;
s.name = "张三" ;//编译错误! 无法访问 name
s.age = 18 ; //编译错误! 无法访问 age
return 0 ;
}
3. 保护继承(protected)
基类的共有成员和保护成员在派生类中的访问权限都会变为保护(protected)权限,在子类中具有访问权限,但是在类的外面则无法访问。
#include <string>
using namespace std;
class person{
public:
string name;
private:
int age;
};
class student: protected person{
//name 和 age保持原有访问权限。
public:
void read(){
s.name = "李四";
s.age = 19 ;
cout << s.age << " 的 " << s.name << " 在看书";
}
};
int main(){
student s;
s.read();
//类的外面无法访问,编译错误。
s.name = "张三" ;
s.age = 18 ;
return 0 ;
}
4. 构造和析构
1. 继承状态
构造函数是对象在创建是调用,析构函数是对象在销毁时调用。但是在继承关系下,无论在对象的创建还是销毁,都会执行父类和子类的构造和析构函数。 它们一般会有以下规则:
a. 子类对象在创建时会首先调用父类的构造函数;
b. 父类构造函数执行完毕后,执行子类的构造函数;
c. 当父类的构造函数中有参数时,必须在子类的初始化列表中显示调用;
d. 析构函数执行的顺序是先调用子类的析构函数,再调用父类的析构函数
#include <iostream>
using namespace std;
class person{
public :
person(){
cout << "调用了父类构造函数" << endl;
}
~person(){
cout << "调用了父类析构函数" << endl;
}
}
class student: public person{
public :
student(){
cout << "调用了子类类构造函数" << endl;
}
~student(){
cout << "调用了子类析构函数" << endl;
}
};
int main() {
Student s1
return 0;
}
2. 继承和组合
如果在继承状态下,子类中的成员又含有其他类的对象属性,那么他们之间的构造很析构调用顺序,遵循以下原则:
a. 先调用父类的构造函数,再调用组合对象的构造函数,最后调用自己的构造函数;
b. 先调用自己的析构函数,再调用组合对象的析构函数,最后调用父类的析构函数。
#include <iostream>
using namespace std;
//父类
class Person{
public :
Person(){
cout << "调用了父类构造函数" << endl;
}
~Person(){
cout << "调用了父类析构函数" << endl;
}
};
//其他类
class A{
public :
A(){
cout << "调用A的构造函数" << endl;
}
~A(){
cout << "调用A的析构函数" << endl;
}
};
//子类
class Student: public Person{
public :
Student(){
cout << "调用了子类类构造函数" << endl;
}
~Student(){
cout << "调用了子类析构函数" << endl;
}
public:
A a;
};
int main() {
Student s1(18 , "zhangsan");
return 0;
}
3. 调用父类有参构造
继承关系下,子类的默认构造函数会隐式调用父类的默认构造函数,假设父类没有默认的无参构造函数,那么子类需要使用参数初始化列表方式手动调用父类有参构造函数。
一般来说在创建子类对象前,就必须完成父类对象的创建工作,也就是在执行子类构造函数之前,必须先执行父类的构造函数。c++ 使用初始化列表来完成这个工作
- 父类
#include <iostream>
using namespace std;
class Person{
private :
int age ;
string name ;
public :
Person(int age , string name){
cout << "调用了父类构造函数" << endl;
this->age = age ;
this->name = name;
}
};
- 子类
子类只能使用初始化列表的方式来访问父类构造
class Student: public Person{
public :
Student(int age , string name):Person(age ,name){
cout << "调用了子类类构造函数" << endl;
}
};
int main(){
Student s1(18 , "zs");
return 0 ;
}
4. 再说初始化列表
初始化列表在三种情况下必须使用:
- 情况一、需要初始化的数据成员是对象,并且对应的类没有无参构造函数
- 情况二、需要初始化const修饰的类成员或初始化引用成员数据;
- 情况三、继承关系下,父类没有无参构造函数情况
初始化列表的赋值顺序是按照类中定义成员的顺序来决定
1. 常量和引用的情况
类中成员为
引用
或者const修饰
的成员
#include <iostream>
#include <string>
using namespace std;
class stu{
public:
const string name; //常量不允许修改值,所以不允许在构造里面使用 = 赋值
int &age; //
stu(string name , int age):name(name),age(age){
cout << "执行构造函数" <<endl;
}
};
int main(){
stu s1("张三" , 88);
cout << s1.name << " = " << s1.age << endl;
return 0 ;
}
2. 初始化对象成员
类中含有其他类的对象成员,如果要初始化,只能使用初始化类列表方式。
#include <iostream>
using namespace std;
class A{
public:
int number;
A(int number):number(number){
cout << "执行了A的构造函数" <<endl;
}
};
class stu{
public:
A a;
stu():a(9){
cout << "执行了stu的构造函数" <<endl;
}
};
int main(){
stu s;
return 0;
}
5. 重写父类同名函数
在继承中,有时候父类的函数功能并不够强大,子类在继承之后,可以对其进行增强扩展。 如果还想调用你父类的函数,可以使用
父类::函数名()
访问
#include <iostream>
using namespace std;
class WashMachine{
public:
void wash(){
cout << "洗衣机在洗衣服" << endl;
}
};
class SmartWashMachine : public WashMachine{
public:
void wash(){
cout << "智能洗衣机在洗衣服" << endl;
cout << "开始添加洗衣液~~" << endl;
//调用父类的函数
WashMachine::wash();
}
};
int main(){
SmartWashMachine s;
s.wash();
return 0 ;
}
6. 多重继承
C++ 允许存在多继承,也就是一个子类可以同时拥有多个父类。只需要在继承时,使用逗号进行分割即可。
using namespace std;
class Father{
public:
void makeMoeny(){
cout << "赚钱" << endl;
}
};
class Mother{
public:
void makeHomeWork(){
cout << "做家务活" << endl;
}
};
class Son:public Father , public Mother{
};
int main(){
Son s ;
s.makeMoeny();
s.makeHomeWork();
return 0 ;
}
1. 多重继承的构造函数
多继承形式下的构造函数和单继承形式基本相同,只是要在子类的构造函数中调用多个父类的构造函数 。 他们调用的顺序由定义子类时,继承的顺序决定。
#include <iostream>
using namespace std;
class Father{
string name;
public:
Father(string name):name(name){
cout << "执行父亲构造函数" <<endl;
}
};
class Mother{
int age;
public:
Mother(int age):age(age){
cout << "执行母亲构造函数" <<endl;
}
};
class Son:public Father , public Mother{
public:
Son(string name ,int age):Father(name),Mother(age){
cout << "执行孩子构造函数" <<endl;
}
};
int main(){
Son s("无名氏" ,38);
return 0 ;
}
7. 类的前置声明
一般来说,类和 变量是一样的,必须先声明然后再使用,如果在某个类里面定义类另一个类的对象变量,那么必须在前面做前置声明,才能编译通过。
class father; //所有前置声明的类,在某个类中定义的时候,只能定义成引用或者指针。
class son{
public:
//father f0; //因为这行代码,单独拿出来说,会执行B类的无参构造,
//但是编译器到此处的时候,还不知道B这个类的构造长什么样。
father &f1;
father *f2;
son(father &f1 , father *f2):f1(f1),f2(f2){
}
};
class father{
};
int main(){
// father b; //---> 执行B的构造函数。
father f1;
father f2;
son s(f1 ,&f2);
return 0 ;
}
七、多态
1. 什么是多态
多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。
C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。为了更为详细的说明多态,此处我们划分为静态多态和 动态多态 两种状态来讲解
1. 静态多态
静态多态是编译器在编译期间完成的,编译器会根据实参类型来选择调用合适的函数,如果有合适的函数可以调用就调,没有的话就会发出警告或者报错 。 该种方式的出现有两处地方: 函数重载 和 泛型编程 | 函数模板 .
int Add(int a, int b){
return a + b;
}
double Add(double a, double b)
{
return a + b;
}
int main()
{
Add(10, 20);
Add(10.0,20.0);
return 0;
}
2. 动态多态
它是在程序运行时根据父类的引用(指针)指向的对象来确定自己具体该调用哪一个类的虚函数。
A *a = new A();
Father * f = new Fahter();
Father * f2 = new Child();
动态多态的必须满足两个条件:
- 父类中必须包含虚函数,并且子类中一定要对父类中的虚函数进行重写。
- 通过父类对象的指针或者引用调用虚函数。
2. 联编机制
1. 父类指针指向子类对象
通常情况下,如果要把一个引用或者指针绑定到一个对象身上,那么要求引用或者指针必须和对象的类型一致。 不过在继承关系下,父类的引用或指针可以绑定到子类的对象,这种现象具有欺骗性,因为在使用这个引用或者指针的时候,并不清楚它所绑定的具体类型,无法明确是父类的对象还是子类的对象。
#include <string>
using namespace std;
class person{
string name;
int age;
};
class student:public person{
};
int main(){
//父类指针指向父类对象
person *p = new person();
//子类指针指向子类对象
student *s = new student();
//父类指针指向子类对象
person *ps = new student();
return 0 ;
}
2. 静态类型和动态类型
只有在继承关系下,才需要考虑静态和动态类型,这里仅仅是强调类型而已。所谓的
静态类型
指的是,在编译时就已经知道它的变量声明时对应的类型是什么。而动态类型
则是运行的时候,数据的类型才得以确定。只有在
引用
或者指针
场景下,才需要考虑 静态或者动态类型。因为非引用或者非指针状态下,实际上发生了一次拷贝动作。
- 静态类型
静态类型:不需要运行,编译状态下即可知晓具体的类型
#include<string>
using namespace std;
int main(){
//编译状态下,即可知晓 a,b的类型。
int a = 3;
string b = "abc";
return 0 ;
}
- 动态类型
只有真正运行代码了,才能知晓具体的类型
class Father{
};
class Child:public Father{
};
//动态类型:
//f在编译时,类型是Father ,但在运行时,真正的类型由getObj来决定。目前不能明确getObj返回的是Father的对象还是Child的对象。
Child getObj(){
Child c ;
return c;
}
int main(){
Father &f = getObj();
return 0 ;
}
3. 访问同名函数
父类的引用或指针可以绑定到子类的对象 , 那么在访问同名函数时,常常出现意想不到的效果。
#include<iostream>
using namespace std;
class father{
public:
void show(){
cout << "father show" << endl;
}
};
class children : public father{
public:
void show(){
cout << "children show" << endl;
}
};
int main(){
father f = children();
f.show(); // 打印father show
return 0 ;
}
4. 静态联编和动态联编
程序调用函数时,到底执行哪一个代码块,走哪一个函数呢?由编译器来负责回答这个问题。将源码中的函数调用解释为执行特定的函数代码,称之为
函数名联编
。
在C语言里面,每个函数名都对应一个不同的函数。但是由于C++里面存在重载的缘故,编译器必须查看函数参数以及函数名才能确定使用哪个函数,编译器可以在编译阶段完成这种联编,在编译阶段即可完成联编也被称为:
静态联编 | 早期联编
。 程序在运行期间才决定执行哪个函数,这种称之为动态联编 | 晚期联编
#include<iostream>
using namespace std;
class WashMachine{
public:
void wash(){
cout << "洗衣机在洗衣服" << endl;
}
};
class SmartWashMachine : public WashMachine{
public:
void wash(){
cout << "智能洗衣机在洗衣服" << endl;
}
};
int main(){
WashMachine *w1= new WashMachine(); //父类指针指向父类对象 打印:洗衣机在洗衣服
w1->wash();
SmartWashMachine *s = new SmartWashMachine(); //子类指针指向子类对象 打印: 智能洗衣机...
s->wash();
WashMachine *w2 = new SmartWashMachine(); //父类指针指向子类对象 打印..洗衣机在洗衣服
w2->wash();
return 0 ;
}
5. 为什么要区分两种联编
动态联编在处理子类重新定义父类函数的场景下,确实比静态联编好,静态联编只会无脑的执行父类函数。但是不能因此就否定静态联编的作用。动态联编状态下,为了能够让指针顺利访问到子类函数,需要对指针进行跟踪、这需要额外的开销。但是并不是所有的函数都处于继承状态下,那么此时静态联编更优秀些。
编写c++代码时,不能保证全部是继承体系的父类和子类,也不能保证没有继承关系的存在,所以为了囊括两种情况,c++ 才提供了两种方式。
正所谓两害相权取其轻,考虑到大部分的函数都不是处在继承结构中,所以效率更高的静态联编也就成了默认的的选择。
练习
一家海洋馆开业了,门口挂着牌子说:欢迎鲨鱼进来游泳。 过了几天,再贴出告示,欢迎鳄鱼也进来游泳,又过了几天,说欢迎罗非鱼也进来游泳,最后干脆直接说了,只要是鱼类的都可以进来游泳。请使用面向对象的思想,设计海洋馆馆接收鱼类的过程。 多态。
//全局函数
void swiming(鲨鱼 , 鳄鱼 , 罗非鱼 , 金鱼, 鳗鱼,… …){
}
3. 虚函数
1. 虚函数入门
C++中的虚函数的作用主要是实现了多态的机制 , 有了虚函数就可以在父类的指针或者引用指向子类的实例的前提下,然后通过父类的指针或者引用调用实际子类的成员函数。这种技术让父类的指针或引用具有欺骗性。
定义虚函数非常简单,只需要在函数声明前,加上
virtual
关键字即可。 在父类的函数上添加 virtual 关键字,可使子类的同名函数也变成虚函数。如果基类指针指向的是一个基类对象,则基类的虚函数被调用 ,如果基类指针指向的是一个派生类对象,则派生类的虚函数被调用。
#include <iostream>
using namespace std;
class WashMachine{
public:
virtual void wash(){
cout << "洗衣机在洗衣服" << endl;
}
};
class SmartWashMachine : public WashMachine{
public:
virtual void wash(){
cout << "智能洗衣机在洗衣服" << endl;
}
};
int main(){
WashMachine *w2 = new SmartWashMachine(); //父类指针指向子类对象 打印..洗衣机在洗衣服
w2->wash();
return 0 ;
}
2. 虚函数的工作原理
了解虚函数的工作原理,有助于理解虚函数。
通常情况下,编译器处理虚函数的方法是: 给每一个对象添加一个隐藏指针成员,它指向一个数组,数组里面存放着对象中所有函数的地址。这个数组称之为虚函数表(virtual function table v-table) 。表中存储着类对象的虚函数地址。
父类对象包含的指针,指向父类的虚函数表地址,子类对象包含的指针,指向子类的虚函数表地址。
如果子类重新定义了父类的函数,那么函数表中存放的是新的地址,如果子类没有重新定义,那么表中存放的是父类的函数地址。
若子类有自己虚函数,则只需要添加到表中即可。
3. 构造函数可以是虚函数吗
构造函数不能为虚函数 , 因为虚函数的调用,需要虚函数表(指针),而该指针存在于对象开辟的空间中,而对象的空间开辟依赖构造函数的执行,这就是鸡和蛋的矛盾问题了。
#include <iostream>
using namespace std;
class father{
public:
virtual father(){ //报错!
cout <<"父亲构造函数~!~" << endl;
}
};
int main(){
father f ;
return 0 ;
}
4. 析构函数可以是虚函数吗
在继承体系下, 如果父类的指针可以指向子类对象,这就导致在使用
delete
释放内存时,却是通过父类指针来释放,这会导致父类的析构函数会被执行,而子类的析构函数并不会执行,此举有可能导致程序结果并不是我们想要的。究其原因,是因为静态联编的缘故,在编译时,就知道要执行谁的析构函数。
为了解决这个问题,需要把父类的析构函数变成虚拟析构函数,也就是加上
virtual
的定义。一旦父类的析构函数是虚函数,那么子类的析构函数也将自动变成虚函数。一句话概括: 继承关系下,所有人的构造都不能是虚函数,并且所有人的析构函数都必须是虚函数。
只要在父亲的析构函数加上 virtual ,那么所有的析构函数都变成 虚函数
#include <iostream>
using namespace std;
class father{
public:
virtual ~father(){
cout << "执行父类析构函数" << endl;
}
};
class son : public father{
~son(){
cout << "执行子类析构函数" << endl;
}
};
int main(){
father *f = new son(); //父类指针指向子类对象
//创建的是子类对象,理应执行子类的析构函数
delete f;
return 0 ;
}
练习
动物都有觅食的行为,但是每种动物吃的食物都不太一样。请使用面向对象的思想,配合虚函数来设计动物觅食的行为。
4. override 关键字
在继承关系下,子类可以重写父类的函数,但是有时候担心程序员在编写时,有可能因为粗心写错代码。所以在C++ 11中,推出了
override
关键字,用于表示子类的函数就是重写了父类的同名函数 。 不过值得注意的是,override
标记的函数,必须是虚函数。
override
并不会影响程序的执行结果,仅仅是作用于编译阶段,用于检查子类是否真的重写父类函数
#include <iostream>
using namespace std;
class father{
public:
virtual void run(){
cout << "父亲在跑步" << endl;
}
};
class son : public father{
public:
virtual void run() override{ //表示重写父类的函数
cout << "孩子在跑步" << endl;
}
};
5. final 关键字
在c++11 推出了final关键字,其作用有两个: (1)、禁止虚函数被重写;(2)、禁止类被继承。
注意: 只有虚函数才能被标记为final ,其他的普通函数无法标记final。
- 标记在类上
class person final{ //表示该类是最终类,无法被继承
};
//编译错误,无法被继承。
class student : public person{
};
- 标记在函数上
class person {
virtual void run() final{ //表示该方法时最终方法,无法被重写
}
};
class student : public person{
//编译错误,方法无法被重写。
void run(){
}
};
6. =delete 和 =default
这两个关键字平常使用的不多,一般出现在类的特殊成员函数上。=delete 用于表示该函数禁止被调用,=default 以及使用编译器默认提供的函数功能。
1. =delete
#include <iostream>
#include <string>
using namespace std;
class stu{
string name;
int age;
public:
stu(string name , int age):name(name) , age(age){
cout << "执行stu的构造函数" << endl;
}
//表示禁止调用拷贝构造函数
stu(stu & s) =delete;
~stu(){
cout << "执行stu的析构函数" << endl;
}
};
int main(){
stu s("张三",18) ;
//编译错误。
stu s1= s;
return 0 ;
}
2. =default
一旦定义了有参构造函数之后,编译器将不会替我们生成无参构造函数 。 此时可以自己编写无参构造函数(哪怕函数体是空的),但是此举增加了程序员的编程工作量。更值得一提的是,手动定义的无参构造函数的代码执行效率要低于编译器自动生成的无参构造函数。
#include <iostream>
#include <string>
using namespace std;
class stu{
string name;
int age;
public:
stu() = default;
stu(string name , int age):name(name) , age(age){
cout << "执行stu的构造函数" << endl;
}
~stu(){
cout << "执行stu的析构函数" << endl;
}
};
int main(){
stu s("张三",18) ;
//编译错误。
stu s1 = s;
return 0 ;
}
7 . 隐式转化问题
在类的有参构造,并且参数只有一个的时候,会有这个问题。 创建对象的时候会有隐式转化的情况。
7. 纯虚函数
纯虚函数是一种特殊的虚函数,C++中包含纯虚函数的类,被称为是“抽象类”。抽象类不能使用new出对象,只有实现了这个纯虚函数的子类才能new出对象。C++中的纯虚函数更像是“只提供声明,没有实现”,是对子类的约束。
纯虚函数就是没有函数体,同时在定义的时候,其函数名后面要加上“= 0”。
#include <iostream>
using namespace std;
class WashMachine{
public:
//没有函数体,表示洗衣机能洗衣服,但是具体怎么洗,每个品牌不一样
virtual void wash() = 0;
};
class HaierMachine:public WashMachine{
public :
virtual void wash(){
cout << "海尔牌洗衣机在洗衣服" << endl;
}
};
class LittleSwanMachine:public WashMachine{
public :
virtual void wash(){
cout << "小天鹅洗衣机在洗衣服" << endl;
}
};
int main(){
//WashMachine w; 错误,抽象类无法创建对象
WashMachine *w1 = new HaierMachine() ;
WashMachine *w2 = new LittleSwanMachine() ;
return 0 ;
}
抽象类的一些特征
- 如果有一个类当中有纯虚函数,那么这个类就是抽象类
- 抽象类是无法创建对象的,因为一旦能够创建对象,里面的纯虚函数没有函数体,也就不知道要执行什么逻辑了,所以禁止抽象类创建对象。
- 抽象类当中也可以有普通的成员函数,虽然父类不能创建对象,但是子类可以创建,所以这些函数可以由子类访问。
- 如果一个子类继承了一个父类(父类是抽象类),那么子类就必须重写所有的纯虚函数,否则视子类为抽象类,因为继承体系下,等同于子类拥有了和父类一样的代码。
8. 抽象类和接口
所谓接口,其实就是用于描述行为和功能,并不会给出具体的实现。C++中没有提供类似
interface
这样的关键字来定义接口 , 纯虚函数往往承担起了这部分功能,可以看成是对子类的一种约束。抽象类可以用来定义一种事物的行为特征,洗衣机: 洗衣服。
鸟会飞。。。 鸡偶尔也会飞、 鸭也会飞,鹅也会飞 , 飞机也会飞。
class Fly{
virtual void fly() = 0 ;
}
class Person{
Person() =default; // 可以用于初始化成员函数
virtual ~Person()=default; //防止子类析构函数无法被调用问题
//每个人吃什么,做什么都不一样,,即可声明为纯虚函数
virtual void eat() = 0 ;
virtual void work() = 0 ;
//...
};
八、智能指针
1. 指针潜在问题
c++ 把内存的控制权对程序员开放,让程序显式的控制内存,这样能够快速的定位到占用的内存,完成释放的工作。但是此举经常会引发一些问题,比如忘记释放内存。由于内存没有得到及时的回收、重复利用,所以在一些c++程序中,常会遇到程序突然退出、占用内存越来越多,最后不得不选择重启来恢复。造成这些现象的原因可以归纳为下面几种情况.
delete 是最容易忘记的。
1. 野指针
出现野指针的有几个地方 :
a. 指针声明而未初始化,此时指针的将会随机指向
b. 内存已经被释放、但是指针仍然指向它。这时内存有可能被系统重新分配给程序使用,从而会导致无法估计的错误
#include <iostream>
using namespace std;
int mian(){
//1. 声明未初始化
int *p1 ;
cout << "打印p1: " << *p1 << endl;
//2. 内存释放后,并没有置空 nullptr
int *p = new int(55);
cout << "释放前打印 : " << *p << endl;
delete p ;
cout << "释放后打印 : " << *p << endl;
return 0 ;
}
2. 重复释放
程序试图释放已经释放过的内存,或者释放已经被重新分配过的内存,就会导致重复释放错误.
int main(){
int *p = new int(4);
//重复释放
delete p;
delete p;
return 0 ;
}
3. 内存泄漏
不再使用的内存,并没有释放,或者忘记释放,导致内存没有得到回收利用。 忘记调用delete
int main(){
int *p = new int(4);
//后面忘记调用delete p;
return 0 ;
}
2. 智能指针
为了解决普通指针的隐患问题,c++在98版本开始追加了智能指针的概念,并在后续的11版本中得到了提升。
在98版本提供的
auto_ptr
在 c++11得到删除,原因是拷贝是返回左值、不能调用delete[] 等。 c++11标准改用unique_ptr
|shared_ptr
|weak_ptr
等指针来自动回收堆中分配的内存。智能指针的用法和原始指针用法一样,只是它多了些释放回收的机制罢了。智能指针位于
#include<memory>
头文件中.
1. unique_ptr
unique_ptr
是一个独享所有权的智能指针,它提供了严格意义上的所有权。也就是只有这个指针能够访问这片空间,不允许拷贝,但是允许移动(转让所有权)。
#include<iostream>
#include <memory>
using namespace std;
int main(){
//1. 创建unique_ptr对象,包装一个int类型指针
unique_ptr<int> p(new int(10));
//2. 无法进行拷贝。编译错误
//unique_ptr<int> p2 = p;
cout << *p << endl;
//3. 可以移动指针到p3. 则p不再拥有指针的控制权 p3 现在是唯一指针
unique_ptr<int> p3 = move(p) ;
cout << *p3 << endl;
//p 现在已经无法取值了。
cout << *p << endl;
//可以使用reset显式释放内存。
p3.reset();
//重新绑定新的指针
p3.reset(new int(6));
//获取到曾经包装的int类型指针
int *p4 = p3.get() ;
//输出6
cout << "指针指向的值是:" << *p4 << endl;
return 0 ;
}
2. shared_ptr
shared_ptr
: 允许多个智能指针共享同一块内存,由于并不是唯一指针,所以为了保证最后的释放回收,采用了计数处理,每一次的指向计数 + 1 , 每一次的reset会导致计数 -1 ,直到最终为0 ,内存才会最终被释放掉。 可以使用use_cout
来查看目前的指针个数
#include <iostream>
#include <memory>
using namespace std;
class stu{
public:
stu(){
cout << "执行构造函数" <<endl;
}
~stu(){
cout << "执行析构函数" <<endl;
}
};
int main(){
shared_ptr<stu> s1 ( new stu());
cout <<" cout = " << s1.use_count() <<endl; //查看指向计数
shared_ptr<stu> s2 = s1;
s1.reset();
s2.reset(); // 至此全部解除指向 计数为0 。 会执行stu的析构函数
return 0 ;
}
3. shared_ptr的问题
对于引用计数法实现的计数,总是避免不了循环引用(或环形引用)的问题,即我中有你,你中有我,
shared_ptr
也不例外。 下面的例子就是,这是因为f和s内部的智能指针互相指向了对方,导致自己的引用计数一直为1,所以没有进行析构,这就造成了内存泄漏。
class father {
public:
father(){cout <<"father 构造" << endl;}
~father(){cout <<"father 析构" << endl;}
void setSon(shared_ptr<son> s) {
son = s;
}
private:
shared_ptr<son> son;
};
class son {
public:
son(){cout <<"son 构造" << endl;}
~son(){cout <<"son 析构" << endl;}
void setFather(shared_ptr<father> f) {
father = f;
}
private:
shared_ptr<father> father;
};
int main(){
shared_ptr<father> f(new father());
shared_ptr<son> s(new son());
f->setSon(s);
s->setFather(f);
}
4. weak_ptr
为了避免
shared_ptr
的环形引用问题,需要引入一个弱指针weak_ptr,它指向一个由
shared_ptr管理的对象而不影响所指对象的生命周期,也就是将一个
weak_ptr绑定到一个
shared_ptr不会改变
shared_ptr的引用计数。不论是否有
weak_ptr指向,一旦最后一个指向对象的
shared_ptr被销毁,对象就会被释放。从这个角度看,
weak_ptr更像是
shared_ptr`的一个助手而不是智能指针。
class father {
public:
father(){cout <<"father 构造" << endl;}
~father(){cout <<"father 析构" << endl;}
void setSon(shared_ptr<son> s) {
son = s;
}
private:
shared_ptr<son> son;
};
class son {
public:
son(){cout <<"son 构造" << endl;}
~son(){cout <<"son 析构" << endl;}
void setFather(shared_ptr<father> f) {
father = f;
}
private:
//shared_ptr<father> father;
weak_ptr<father> father; //替换成weak_ptr 即可。
};
int main(){
shared_ptr<father> f(new father());
shared_ptr<son> s(new son());
f->setSon(s);
s->setFather(f);
}
九、动态内存
1. 内存分区
在C++中内存分为5个区,分别是
堆
、栈
、全局/静态存储区
和代码|常量存储区
|共享内存区
。
栈区:又叫堆栈,存储非静态局部变量、函数参数、 返回值等,栈是可以向下生长的
共享内存区:是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内 存,做进程间通讯
堆区:用于程序运行时动态内存分配,堆是可以向上增长的
静态区:存储全局数据和静态数据
代码区:存储可执行的代码、只读常量
2. new 和 delete
在 c++ 中 , 如果要在堆内存中申请空间,那么需要借助
new
操作符,释放申请的空间,使用delete
操作 。而c语言使用的是malloc
和free
,实际上new
和delete
的底层实际上就是malloc
和free
。
1. new
在c++中, new是一个关键字,同时也是一个操作符,用于在堆区申请开辟内存 , new的操作还具备以下几个特征:
- 内存申请成功后,会返回一个指向该内存的地址。
- 若内存申请失败,则抛出异常,
- 申请成功后,如果是程序员定义的类型,会执行相应的构造函数
#include <iostream>
using namespace std;
class stu{
stu(){
cout << "执行构造函数" <<endl;
}
~stu(){
cout << "执行析构函数" <<endl;
}
}
int main(){
int *a = new int();
stu *s = new stu();
//new的背后先创建
return 0 ;
}
2. delete
在c++中,
delete
和new
是成对出现的,所以就有了no new no delete
的说法。delete
用于释放new
申请的内存空间。delete
的操作具备以下几个特征:
- 如果指针的值是0 ,delete不会执行任何操作,有检测机制
- delete只是释放内存,不会修改指针,指针仍然会指向原来的地址
- 重复delete,有可能出现异常
- 如果是自定义类型,会执行析构函数
int main(){
int *p = new int(6);
delete p ; // 回收数据
*p = 18 ; //依然可以往里面存值,但是不建议这么做。
return 0 ;
}
3. malloc 和 free
malloc` 和 free 实际上是C语言 申请内存的语法,在C++ 也得到了保存。只是与 new 和 delete 不同的是, 它们 是函数,而 new 和 delete是作为关键字使用。 若想使用,需要导入`#include
- malloc
- malloc 申请成功之后,返回的是void类型的指针。需要将void*指针转换成我们需要的类型。1.
- malloc 要求制定申请的内存大小 , 而new由编译器自行计算。
- 申请失败,返回的是NULL , 比如: 内存不足。
- 不会执行自定义类型的构造函数
int main(){
int *p=(int *)malloc(int); //如果申请失败,返回的是NULL
return 0 ;
}
- free
free 和 malloc是成堆出现的,所以也有了 no malloc no free的说法。 free 用于释放 mallo申请的内存空间。
- 如果是空指针,多次释放没有问题,非空指针,重复释放有问题
- 不会执行对应的析构
- delete的底层执行的是free
int main(){
int *p=(int *)malloc(int); //如果申请失败,返回的是NULL
free(p);
return 0 ;
}