目录
类和对象的概述
C++是面向对象的语言,对象其实可理解成客观事物,C++是面向过程的语言,他注重的是一步步过程。比如把一头大象塞进冰箱,C语言执行过程有三步,打开冰箱门,把大象塞进去,然后关上冰箱门。C++只关注对象,把大象塞进冰箱,有三个对象,人和大象以及冰箱,整个过程是人,大象,冰箱三个对象之间交互完成的,人不需要关注大象是怎么塞进冰箱的,冰箱是怎么关门的。在面向过程的编程中,程序员需要关注每一个步骤和细节,手动控制整个过程的执行。而在面向对象的编程中,程序员将问题抽象成对象,每个对象负责自己的行为和状态,通过对象之间的交互来完成任务,程序员只需要关注对象之间的关系和交互,而不需要关心具体的实现细节。
什么是类呢,类是一种抽象的概念,用来描述具有相似属性和行为的一组对象,通俗来讲上面的塞冰箱的大象可以有亚洲象,非洲象等很多不同名字的大象,但是他们都是大象这一类,都是四条腿,长鼻子(也许有例外)。
C语言结构体与C++类的区别
C语言结构体只能定义变量,在C++中,类不仅可以定义变量,也可以定义函数
类的例子
c语言结构体的例子
c语言结构里放函数直接会报错
所以函数只能放结构体外面
类和结构体写法都差不多,都是类的类型class(如果是结构体的话就是struct)加上类名(结构体名)然后加上花括号加逗号{相关内容};
C++结构体和C++类的区别
C++的结构体和C语言的结构体有很大的区别,C++结构体和类很像,也可以在结构体内部写函数,区别是访问限定符,C++结构体是默认公有public,而类是默认私有的private
类的访问限定符和封装
public(公有) | private(私有) | protected(保护) |
在类外可以直接访问 | 在类外不能直接访问 | 在类外不能直接访问 |
访问权限作用域是从该访问限定符出现的位置开始直到下一个访问限定符出现为止
如果第一个访问限定符一直到最后也没有遇到下一个访问符,那么它的作用符就一直到结尾
在C++中,类的访问权限是通过 public、private 和 protected 关键字来限制的。这种限制是为了实现封装性和数据隐藏,是面向对象编程中的重要概念之一。
封装性:通过将类的数据和方法封装在类的内部,并限制外部对其访问的方式,可以确保类的内部实现细节不会被外部直接访问和修改。这样可以有效地保护数据的完整性和安全性,同时也可以减少对类的用户的依赖,使得类的实现细节可以更灵活地被修改而不会影响外部代码。
数据隐藏:将类的数据成员定义为私有(private)的,可以防止外部直接访问和修改类的数据,只能通过类的公有(public)方法来访问和修改数据,从而确保数据的安全性和一致性。
继承和多态:限制访问权限也有利于继承和多态的实现。protected成员可以被派生类访问,但不能被外部访问,这样可以保证派生类对基类的访问权限。
通过限制访问权限,可以实现类的封装和数据隐藏,提高了代码的安全性、可维护性和灵活性。
一般来讲,类里面的成员变量用private修饰,不让随意访问,而成员函数用public来修饰,只有通过公有的函数来访问成员变量。
主要为了隐藏数据和安全性
数据隐藏:将成员变量定义为私有(private)的,可以防止外部直接访问和修改类的数据,只能通过类的公有(public)方法(函数)来访问和修改数据。这样可以确保数据的安全性和一致性,避免外部直接操作数据导致数据被破坏或不一致。
封装性:将成员函数定义为公有(public)的,可以提供外部访问类的接口,外部代码可以通过这些公有方法来操作类的数据和实现功能。这样可以隐藏类的内部实现细节,只暴露必要的接口给外部,降低了类与外部代码之间的耦合度,提高了代码的模块化和可维护性。
安全性:通过将成员变量私有化,可以在类的内部实现对数据的合法性检查和控制,确保数据的有效性和安全性。同时,通过公有方法提供的接口,可以实现对数据的访问和操作的控制,实现更加安全的数据处理。
类的实例化
用类类型创建对象的过程,称为类的实例化,类是对对象进行描述的一个抽象的东西,是一个模型一样的东西,限定了类有那些成员。比如说大象类和非洲象,大象限制了四条腿,长鼻子,但是大象是个抽象的概念,非洲象是大象的一个实例。
类就像谜语一样,对谜底来进行描述,谜底就是迷语的一个实例
在C++里,类实例化可以不加上class类型标志,直接类名+变量名就可以完成实例化了 。在 C++ 中使用类名加上变量名来实例化对象时,编译器会根据上下文环境进行类型推断,自动将类名解析为类类型。这种类型推断的机制称为类名查找(class name lookup),编译器会在当前作用域中查找与类名匹配的类定义,然后将类名解析为对应的类类型。因此,即使没有显式地指定类的类型标志,编译器也能够正确地识别类名并完成实例化。
其实C++结构体变量创建也可以直接使用结构体名来创建,不需要再写struct类型标志
但是在C语言里不能直接使用结构体类型名去创建变量
不写struct直接报错
除非用typedef 把struct student直接重命名为struct,这也是C语言描述数据结构里常用的写法
类的作用域以及类的成员函数访问
类的成员函数可以定义和声明都在类里,也可以声明在类里,定义在类外。类的成员函数如果声明和定义都写在类里,那么不管写不写内联函数的标志符inline编译器都会直接默认把成员函数默认认为是内联函数。
类内部定义成员函数,默认成员函数是内联
类定义了一个新的作用域,在类外定义成员函数时,需要使用::作用域操作符指明成员属于哪个类域,与命名空间访问成员挺像的
类的大小计算
类的大小计算同样适用于结构体对齐规则
第一个成员在与结构体偏移量为0的地址处(从0开始算起)
其他成员变量要对齐到对齐数的整数倍的地址处,对齐数=编译器默认的一个对齐数与该成员类型大小的较小值。vs编译器默认的对齐数是8
结构体总大小为最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
如果是指针类型的话,32位指针一律是4个字节,64位一律是8个字节,不管是什么类型的指针;
使用offsetof可以查看每个成员开始存储的位置偏移量
offsetof (type,member),type是结构体或者类类型名,member是成员名,头文#include<stdlib.h>
默认对齐数是编译器直接给的,可以使用 #pragma pack
修改默认的对齐数。#pragma pack
指令可以指定结构体成员的对齐方式,即结构体成员在内存中的对齐方式。
#include <stdio.h>
// 指定结构体成员的对齐方式为 1 字节
#pragma pack(1)
struct student
{
int chinese;
int math;
struct s
{
long i;
double c;
};
};
// 恢复默认对齐方式
#pragma pack()
int main()
{
struct student xiaoming;
printf("%zu\n", sizeof(xiaoming)); // 输出修改后的结构体大小
return 0;
}
为什么要结构体对齐呢,或者类对齐
结构体对齐是为了提高内存访问的效率和性能。在计算机系统中,CPU 访问内存时通常是按照内存地址的整数倍进行的,而不是按照字节一个一个地访问。因此,如果结构体的成员变量没有进行对齐,可能会导致 CPU 访问内存时需要多次访问,增加访存时间,降低系统性能。
通过内存结构体对齐,可以使得结构体的起始地址是成员变量大小的整数倍,从而保证结构体的每个成员变量都能够按照 CPU 的要求进行访问,减少内存访问的次数,提高访问效率。结构体对齐可以通过编译器的对齐指令或者编译选项来实现,编译器会在编译时根据平台的要求对结构体进行对齐处理
通俗点讲就是空间换时间
上面的情形类基本都适用,但是类里面是可以存放成员函数的啊,成员函数的大小怎么计算
加了函数之后可以看出,类的大小依旧是 24,这说明成员函数压根就不加入到类大小计算当中去。这是因为函数压根就不存储在类的每一个对象里,它存在一个公共的代码区里,所有对象都共享相同的函数代码。
this指针
类实例化后,会给对象分配存储空间,但是成员函数不会分配空间,他们是只用了一段空间来存放这个公共函数代码段。所有的对象调用的都是相同内容的代码,每个对象都有属于自己的数据成员,但是所有的对象的成员函数却合用一份成员函数。那么成员函数是怎么辨别出当前调用自己的是哪个对象呢,其实就是通过this指针来辅助辨别的,这个指针叫做自引用指针。每当创建一个对象时,系统就把this指针初始化指向该对象,即this指针是当前调用成员函数的对象的起始地址。每当调用函数时,编译器就会把当前调用成员函数的地址给this指针,当成员函数想要使用成员变量时会通过this指针找到是哪个对象调用的函数,从而去找这个指针的成员变量
举个例子
#include<iostream>
using namespace std;
class A
{
public:
A(int x1)
{
x = x1;
}
void diap()
{
cout << "x=" << x << endl;
}
private:
int x;
};
int main()
{
A a(1), b(2);
cout << "a:";
a.diap();
cout << "b:";
b.diap();
return 0;
}
为什么disp能够区分开a和 b 正确打印值,这是因为diap实际上相当于
void diap(A*this)
{
cout << "x=" << this->x << endl;
}main函数里是这样的
int main()
{
A a(1), b(2);
cout << "a:";
a.diap(&a);
cout << "b:";
b.diap(&b);
return 0;
}
每次调用的时候把要调用这个函数的对象地址传过去,然后通过this->就可以访问到这个对象的所有成员变量了,但是这些工作都是编译器帮忙做的,我们是不需要写取地址和函数参数部分显式地写this的。
但是函数内部使用this内部显式地写this访问变量是可以写出来,也可以不写出来
不写出来编译器会帮你处理
示例
#include<iostream>
using namespace std;
class A
{
public:
A(int x1)
{
x = x1;
}
void diap()
{
cout << "访问了diap函数" << endl;
}
private:
int x;
};
int main()
{
A* ppt = nullptr;//空指针
ppt->diap();
}
为什么上面传个空指针过去不会报错,因为指针也有自己的地址,空指针只是它内部的内容保存的是空,不代表他的地址是空的,所以传ppt这个指针变量过去用this指针传过去不会报错,但是一旦使用this指针,this->或者解引用就会报错。空指针可以访问但是不能解引用
类的6个默认构造函数
构造函数
构造函数就是用来初始化对象的函数,不是构造空间,名字与类名相同,创建类类型对象由编译器自动调用,构造函数没有任何返回值包括void也不用写。
为什么类不像C语言结构体变量一样直接初始化呢,而是要写一个构造函数来初始化呢
结构体初始化
C语言结构体变量能直接初始化的前提是它没有访问限定符这一概念,所以可以随意访问结构体的成员来实现初始化。而类里面成员变量一般默认设置为私有,在外面是直接访问不到的,只有通过公有的成员函数来访问成员变量,所以要通过给成员函数传参来间接给成员变量初始化。
构造函数重载与默认构造函数
构造函数和普通函数一样可以使用重载,根据不同参数的类型,大小和顺序来调用不同的函数来实现不同的初始化。构造函数可以有多个
无参构造时不能使用(),因为函数声明也是这么写的,编译器会认不清是无参调用构造函数,还是另一个函数的声明
#include<iostream>
#include<string>
using namespace std;
class student
{
public:
student(int age, string name, int weight, int height)//构造函数初始化
{
_age = age;
_name = name;
_weight = weight;
_height = height;
}
student()
{
cout << "无参构造成功" << endl;//无参调用构造函数时打印提示信息
}
void Printf()//打印函数,方便查看数据
{
cout <<"年龄"<< _age <<"姓名" << _name<<"体重" << _weight <<"高度" << _height<<endl;
}
private:
int _age;
string _name;
int _weight;
int _height;
};
int main()
{
student xiaoming(20,"xiaoming", 120, 170);//有参构造
xiaoming.Printf();
student xiaohei;//调用无参构造函数
}
如果类中没有显示定义的构造函数,那么编译器会自动形成一个无参构造,无参构造因为没有给值进行初始化,所以虽然有默认的构造函数,但是依旧是随机值,没有完成初始化
无参的构造函数,全缺省的构造函数都叫做默认构造函数,不传参就可以调用的就是默认构造函数。如果你显式写了有参的构造函数,那么就不会自动调编译器给的默认无参构造
默认构造函数不会初始化内置类型,但是会去初始化自定义类型,比如结构体或者别的类
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
using namespace std;
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
上面代码里默认构造函数自动调用了自定义类型的默认构造函数进行初始化了,但是如果自定义类没有默认构造函数,是一个有参的构造函数或者没有构造函数,那么他依旧不会初始化成功,依旧是随机值
默认构造函数暂时的作用就是用来自动调用自定义类型的默认构造函数对他进行初始化,最底层还是要自己写构造函数。如果自定义函数没有默认构造函数就不会完成初始化,同时默认构造函数不会去处理内置类型,int char之类的;
因为它不会去处理内置类型,所以给类的变量引入了缺省的概念,就是你可以在声明的时候直接给值,如果是默认构造函数不会对内置类型进行处理赋值,那么这些内置类型变量会直接用缺省值
析构函数
构造函数是完成初始化的函数,相当于C语言数据结构里写的init函数,析构函数则是完成对象中资源的清理工作,相当于destory,但是不是对局部对象的销毁。假如这个对象里的成员变量用malloc申请了空间,那么就需要析构函数来销毁。但是这个对象实例化占的空间是在程序结束后由编译器完成的
C语言初始化栈的写法以及destory写法
typedef struct stack
{
int* arry;
int _capacity;
int _top;
}stack;
void init(stack *q)
{
q->arry = (int*)malloc(sizeof(int) * capacity);
q->_capacity = 100;
q->_top = 0;
}
void destory(stack* q)
{
free(q->arry);
q->arry = NULL;
q->_top = 0;
}
C++构造函数和析构函数写栈的写法
class stack
{
stack(int capacity)//构造函数
{
arry = (int*)malloc(sizeof(int) * capacity);
if (arry == NULL)
{
perror("malloc fail\n");
exit(1);//退出程序,返回1,正常返回0
}
_capacity = capacity;
_top = 0;
}
~stack()//析构函数
{
free(arry);
arry = NULL;
_capacity = 0;
_top = 0;
}
private:
int* arry;
int _capacity;
int _top;
};
int main()
{
stack q(100);
}
上面栈的析构函数不会回收对象q的空间,而是q的成员变量arry malloc申请的空间,对象q的空间在程序结束直接回收
如果没有malloc申请空间,那么可以不用写析构函数,因为不需要资源回收。但是如果申请了空间又不写析构函数就会造成空间泄漏。虽然有默认析构函数,但是默认析构函数只会对自定义类型进行析构
class stack
{
public:
stack()//构造函数
{
cout << "调用了stack的默认构造函数" << endl;
arry = (int*)malloc(sizeof(int) * 50);
if (arry == NULL)
{
perror("malloc fail\n");
exit(1);//退出程序,返回1,正常返回0
}
_capacity = 50;
_top = 0;
}
~stack()//析构函数
{
cout << "调用了stack的析构函数" << endl;
free(arry);
arry = NULL;
_capacity = 0;
_top = 0;
}
private:
int* arry;
int _capacity;
int _top;
};
class queue
{
private:
stack q;
stack t;
int tamp=0;
};
int main()
{
queue A;
}
queue这个类虽然没写构造函数,但是里面都是自定义类型stack,所以queue会用默认构造函数以及默认析构函数 去调默认stack的构造函数和析构函数
queue A销毁时,要将其内部的两个栈stack q和stack t对象销毁,但是main函数不能直接调用stack的析构函数,因为实际要析构的是queue的资源,,所以实际调用的是queue的析构函数。但是并没有显示写queue的析构函数,所以编译器会生成一个默认的析构函数,main函数通过queue的析构函数才能分别去stack的析构函数完成stack的析构,最后完成queue的析构。queue类里还有一个内置类型tamp,内置类型不需要资源清理,最后系统会直接回收。
拷贝构造
拷贝构造是另一种形式重载构造函数,构造函数是用来初始化对象的函数。拷贝构造也是用来初始化对象的函数,只是构造函数传参是自己给的数值,而拷贝构造是把一个已经实例化初始化后的对象它里面的所有成员变量的值全复制一份给另一个对象初始化
下面的例子第一个是有参构造函数,第二个是拷贝构造函数
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
using namespace std;
class student
{
public:
student(int age, string name, int weight, int height)//构造函数初始化
{
_age = age;
_name = name;
_weight = weight;
_height = height;
}
student(const student& s)//拷贝构造
{
cout << "拷贝构造成功" << endl;
_age = s._age;
_name = s._name;
_weight = s._weight;
_height = s._height;
}
void Printf()//打印函数,方便查看数据
{
cout << "年龄" << _age << "姓名" << _name << "体重" << _weight << "高度" << _height << endl;
}
private:
int _age;
string _name;
int _weight;
int _height;
};
int main()
{
student xiaoming(20, "xiaoming", 120, 170);//有参构造
xiaoming.Printf();
student xiaohei(xiaoming);
xiaohei.Printf();
}
为什么构造函数传参只用传值传参就可以了,为什么拷贝构造要传个引用
student(const student s)//拷贝构造,如果拷贝构造是传值传参,形参是实参的一份临时拷贝,main调用的时候是student xiaohei(xiaoming);也就是说student s=xiaoming,student s在之前并没有定义出来,所以会拿xiaoming这个对象的值再拷贝构造一次,也就是再走一次student(student s),然后又是形参是实参的一份临时拷贝,又来一遍拷贝构造。也就是一直循环拷贝构造下去,这样就直接死循环了
如果是用引用的话,student(const student& s)//拷贝构造,相当于cosnt student &s=student xiaoming; s是xiaoming的一个别名,s就相当于xiaoming,xiaoming这个对象在使用拷贝构造之前就已经通过构造函数初始化了并且实例化了,也就相当于s实例化出来了,s不用调任何构造函数或者拷贝构造
浅拷贝与默认拷贝构造
如果你不显式地写拷贝构造,编译器也会生成一个默认的拷贝构造
#include<iostream>
#include<string>
using namespace std;
class student
{
public:
student(int age, string name, int weight, int height)//构造函数初始化
{
_age = age;
_name = name;
_weight = weight;
_height = height;
}
void Printf()//打印函数,方便查看数据
{
cout << "年龄" << _age << "姓名" << _name << "体重" << _weight << "高度" << _height << endl;
}
private:
int _age;
string _name;
int _weight;
int _height;
};
int main()
{
student xiaoming(20, "xiaoming", 120, 170);//有参构造
xiaoming.Printf();
student xiaohei(xiaoming);
cout<<"小黑的结果"<<endl;
xiaohei.Printf();
}
把拷贝构造删了依旧可以复制成功 ,和析构函数以及构造函数不同,他连内置类型也会一起处理
但是默认拷贝构成出来的都是浅拷贝,浅拷贝是指内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的
但是浅拷贝是有缺陷的,如果涉及到里面的成员变量要malloc生成空间,比如说栈要用malloc函数来申请空间,成员变量arry保存的是这块空间的地址,此时进行默认拷贝构造会把arry保存的地址原封不动地复制到另一个对象的成员变量里。这样就造成了两个指针指向同一块空间,而析构函数是按构造函数相反的顺序来进行析构的,也就是前一个先构造的后析构,后面构造的先析构。后面哪个析构完了,前面的也会自动调用析构,那么就造成了同一块空间析构释放了两次,这样程序就会崩溃
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
/*注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请
时,则拷贝构造函数是一定要写的,否则就是浅拷贝。*/
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType *_array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}
这种设计到资源申请的malloc之类的就一定要用深拷贝,其实也就是自己写一个拷贝构造去手动分配开辟空间
Stack(const Stack& s)
{
cout << "拷贝构造" << endl;
_array = (DataType*)malloc(s._capacity * sizeof(DataType));//但是要注意开辟空间大小一样的
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}//涉及到开辟空间的就自己写,剩下的直接赋值就可以了
_capacity = s._capacity;
_size = s._size;
}
总结一下,如果没有管理资源,一般不要写拷贝构造,默认生成的拷贝构造构造就可以
如果都是自定义类型成员,内置类型成员没有指向资源,类似默认生成拷贝构造就可以 ,因为如果都是自定义类型,默认拷贝构造会使他们自己去调自己的拷贝构造,如果有两个自定义类型成员,那么也就是调两次,即使他们都要开辟空间,但是因为是前后调了两次,所以开辟的空间地址不会相同
一般情况下不要写析构函数就不用写拷贝构造,因为他们都是涉及到资源分配才会写
如果内部有指针指向空间,就要写深拷贝,如链表之类的