文章目录
C++ 基础入门
1、c++书写规范
#include <iostream>
using namespace std;
int main(){
system("pause");
return 0;
}
2、常量
作用:用于记录程序中不可更改的数据
/*
通常在文件上方定义,表示一个常量
#define 宏常量:#define 常量名 常量值
通常在变量定义前加关键字 const,修饰该变量为常量,不可修改
const 修饰的常量:const 数据类型 常量名 = 常量值
*/
#include <iostream>
using namespace std;
//常量定义方式
//1.#define 宏常量
//2.const 修饰的变量
//1. #define 宏常量
#define Day 7
int main() {
cout << "一周总共有:" << Day << "天" << endl; //Day = 14; ×:Day是常量 ,一旦修改就会报错
//2. const 修饰的变量
const int month = 12;
cout << "一年总共有:" << month << "月" << endl; //month = 24; ×:const修饰的变量也为常量
system("pause");
return 0;
}
3、实型(浮点型)
#include <iostream>
using namespace std;
int main() {
//1.单精度 float
//2.双精度 double
//默认情况下,输出一个小数,会显示出6位有效数字(小数点前的也算进去)
float f = 3.1415926f;
double d = 3.1415926;
cout << "f = " << f << endl; //3.14159
cout << "d = " << d << endl; //3.14159
//float 所占字节为4;double所占字节为8
cout << "float所占字节:" << sizeof(f) << endl; //4
cout << "double所占字节:" << sizeof(d) << endl; //8
//科学计数法
float f2 = 3e2; //3*10^2
cout << "f2 = " << f2 << endl; //300
float f3 = 3e-2; //3*0.1^2
cout << "f3 = " << f3 << endl; //0.03
system("pause");
return 0;
}
4、字符串型
作用:用于表示一串字符
两种风格
/*
1.c风格字符串
注意事项1 变量名后边一定要有 []
注意事项2 等号后面要用双引号将字符串括起来
2.C++风格字符串
注意事项 必须包含头文件 #include<string>
*/
#include <iostream>
using namespace std;
#include <string>
int main() {
char str1[] = "hello world";
cout << str1 << endl;
string str2 = "hello C++";
cout << str2 << endl;
return 0;
}
5、布尔类型
作用:代表真或假的值
bool类型只有两个值
- true — 真(本质是:1)
- false — 假(本质是:0)
注意:bool只占一个字节和char类型一样
6、数据的输入
作用:用于从键盘获取数据
关键字 cin
语法 :cin >> 变量
#include <iostream>
using namespace std;
#include <string>
int main() {
//1.整型
int a = 0;
cout << "请给整型变量a赋值:" << endl;
cin >> a;
cout << "整型变量a=" << a << endl;
//2.浮点型
float f = 3.14f;
cout << "请给浮点变量f赋值:" << endl;
cin >> f;
cout << "浮点变量f=" << f << endl;
//3.字符型
char ch= 'a';
cout << "请给字符变量ch赋值:" << endl;
cin >> ch;
cout << "字符ch=" << ch << endl;
//4.字符串型
string str = "hi";
cout << "请给字符串变量str赋值:" << endl;
cin >> str;
cout << "字符串str=" << str << endl;
//5.布尔类型
bool flag = false;
cout << "请给布尔类型变量falg赋值:" << endl;
cin >> flag;
cout << "布尔flag=" << flag << endl;
return 0;
}
7、 赋值运算符
运算符 | 术语 | 示例 | 结果 |
---|---|---|---|
= | 赋值 | a = 2;b = 3 | a = 2;b = 3; |
+= | 加等于 | b = 0;a+=2; | a = 2; |
-= | 减等于 | a = 5;a-=3; | a = 2; |
*= | 乘等于 | a = 2;a*=2; | a = 4; |
/= | 除等于 | a = 4;a/=2; | a = 2; |
%= | 模等于 | a = 3;a%=2; | a = 1; |
8、三目运算符
通过三目运算符进行简单的判断
语法:表达式1 ? 表达式2 : 表达式3
解释
如果表达式1为真,执行表达式2,并返回表达式2的结果
如果表达式1为假,执行表达式3,并返回表达式3的结果
#include <iostream>
using namespace std;
int main() {
int a = 10;
int b = 20;
int c = 0;
c = a > b ? a : b;
cout << c << endl; //20
a > b ? a : b = 100; //在C++中三目运算符返回的是变量,可以继续赋值
cout << a << endl; //10
cout << b << endl; //100
return 0;
}
9、goto语句
作用:可以无条件跳转语句
语法: goto 标记
解释:如果标记的名称存在,执行到goto语句时,会跳转到标记位置
#include <iostream>
using namespace std;
int main() {
cout << "1" << endl;
cout << "2" << endl;
goto FLAG;
cout << "3" << endl;
cout << "4" << endl;
FLAG:
cout << "5" << endl;
system("pause");
return 0;
}
//输出 1 2 5
10、二维数组
/*
二维数组定义方式
1.数据类型 数组名[行数][列数];
2.数据类型 数组名[行数][列数] = {{数据1,数据2},{数据3,数据4}};
3.数据类型 数组名[行数][列数] = {数据,数据2,数据3,数据4};
4.数据类型 数组名[][列数] = {数据,数据2,数据3,数据4};
1.数据类型 数组名[行数][列数];
int arr[2][3];
arr[0][0] = 1;
arr[0][1] = 2 ;
arr[0][2] = 3;
arr[1][0] = 4;
arr[1][1] = 5;
arr[1][2] = 6;
2.数据类型 数组名[行数][列数] = {{数据1,数据2},{数据3,数据4}};
int arr[2][3] =
{
{1,2,3},
{4,5,6}
};
3.数据类型 数组名[行数][列数] = {数据,数据2,数据3,数据4};
int arr[2][3] ={1,2,3,4,5,6 };
4.数据类型 数组名[][列数] = {数据,数据2,数据3,数据4};
int arr[][3] = { 1,2,3,4,5,6 };
*/
#include <iostream>
using namespace std;
int main() {
int arr[][3] = { 1,2,3,4,5,6 };
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
cout << arr[i][j] << " ";
}
cout << endl;
}
return 0;
}
命名空间 namespace
作用:防止变量或函数等重名,引起访问不明确
访问方式
//方式1:命名空间 :: 函数名();
pointer1::print();
//方式2:using namespace pointer1;(谨慎使用)
using namespace pointer1;
如何区分全局内容和命名空间中的内容
pointer1::print();//命名空间中的内容
::print();//全局内容
代码举例
#include<iostream>
using namespace std;
//定义一个函数空间
namespace pointer {
int a = 10;
void printxx() {
cout << a << endl;
}
}
//定义一个函数
void printx() {
int b = 20;
cout << b << endl;
}
int main() {
//当调用的函数没在命名空间,使用 :: 直接引用
::printx();
//引用:命名空间::函数名
pointer::printxx();
return 0;
}
/*
20
10
*/
输入输出函数
cout
作用:输出
cin
cin 是对象
作用:输入
endl
作用:清空缓冲区并换行
清空缓冲区:endl 前的变量数据存在 RAM 中,执行endl意味着立刻将RAM 的数据刷新到ROM中,及时清理缓冲区
#include<iostream>
using namespace std;
int main(){
int n = 100 ;
cout << n << endl; //清空缓冲区并换行
return 0;
}
引用
特点(很重要)
- 引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)
- 不能有NULL引用,引用必须与合法的存储单元关联(指针则可以是NULL)
- 一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)
![微信截图_20210104214805](https://img-blog.csdnimg.cn/img_convert/85140f86682d185aa1fa41887e886c22.png
类
1、类和结构体的区别
class与struct在编译过程中基本没有区别,只是缺省权限不同class是私有的struct是公有的;
公司开发里开发时,struct往往用于纯数据的结构体,把class当作多功能类的开发。
2、访问修饰符:private,protected,public
private:私有权限的成员变量和成员函数,只能由该类中的函数,其友元函数访问,不能被任何其他访问,该类的对象不能访问;
protected:可以被该类中的函数,子类的函数,以及其友元函数访问,但不能被该类的对象访问
public:公有权限的成员变量和成员函数,可以被该类中的函数,子类的函数,其友元函数访问,也可以由该类的对象访问
3、对象
类和对象区别:定义的变量的类型叫类,定义的变量的名字叫对象。类是抽象的,对象是具体的。
属性(成员变量)
行为(成员函数)
#include <iostream>
#include <string>
using namespace std;
//结构体
struct SSS {
public:
int a;
char c;
};
//类
class People {
//private出现原因:安全
//protected: 保护
//public: 公有
private: //属性一般私有
int age;//属性(成员变量)
int weight;
public: //行为一般公有
void setAge() {
age++;
}
int getAge() {//行为(成员函数)
return age;
}
void setWeight(int w) {
if (w < 0 || w >= 1000) {
printf("输入体重有误\n");
weight = 0;
return;
}
weight = w;
}
int getWeight() {
return weight;
}
};
int main() {
People ss; //创建对象
int a = ss.getAge();
ss.setWeight(-2);
cout << ss.getWeight() << endl;
return 0;
}
/*
输入体重有误
0
*/
4、函数重载
相同的声明域函数名字相同但是参数列表不同(类型和个数不完全相同)
只有返回值类型不同不算重载函数
#include <iostream>
#include <string>
using namespace std;
class People {
private:
int age;
int weight;
public:
//函数重载setWeight()
void setWeight() {
weight = 0;
}
//函数重载setWeight(int w)
void setWeight(int w) {
weight = 1;
}
//函数重载setWeight(int w,int w2)
void setWeight(int w,int w2) {
weight = 2;
}
int getWeight() {
return weight;
}
};
int main() {
People ss;
ss.setWeight(100);
cout << ss.getWeight() << endl;
return 0;
}
5、构造函数
构造函数是类的一种特殊成员函数,一般情况下,构造函数是专门用于初始化成员变量的,所以最好不要在构造函数中进行与对象的初始化无关的操作。
构造函数的函数名一定与类的名称完全相同,而且没有返回值(不是说返回值是void或者int,而是说不允许指定返回值)。
创建对象的时候会默认调用构造函数
#include <iostream>
#include <string>
using namespace std;
//类
class People {
//private出现原因:安全
private:
int age;//属性(成员变量)
int weight;
public:
/*
默认会有构造函数,里面什么也没有
People() {
}
*/
//将上面的构造函数覆盖
People() {//构造函数可以重载
age = 1;
}
People(int weight) {
age = 1;
//当形参名和成员变量重名时需要用 this指向成员变量来表示成员变量
//this 是调用这个函数的对象的地址,默认存在
this->weight = weight;
}
int getAge() {
return age;
}
int getWeight() {
return weight;
}
};
int main() {
//创建对象就会执行构造函数
//构造函数也可函数重载
//People ss; //调用People()
People ss2(8);//调用People(int w)
cout << ss2.getWeight() << endl;
return 0;
}
6、 this
区别成员变量与局部变量
只能在类成员函数中使用,指向调用该成员函数的对象
#include <iostream>
#include <string>
using namespace std;
class People {
private:
int age;
int weight;
public:
People(int weight) {
age = 1;
//当形参名和成员变量重名时需要用 this指向成员变量来表示成员变量
//this 是调用这个函数的对象的地址,默认存在
this->weight = weight;
}
int getWeight() {
return weight;
}
};
int main() {
People ss2(8);
cout << ss2.getWeight() << endl;
return 0;
}
7、 析构函数:~
对象销毁的时候调用析构函数
不支持函数重载
默认存在
一个类可以有多个构造函数,却只能有一个析构函数
栈区对象,先创建的对象后销毁,后创建的对象先销毁
#include <iostream>
#include <string>
using namespace std;
class People {
private:
int age;
int weight;
char* name;
public:
People() {
name = (char*)malloc(100);
strcpy(name, "123456");
}
~People() {//主要用于释放空间,不能有参数,不能重载
free(name);
}
};
int main() {
return 0;
}
8、 初始化列表(记住格式)
People() :age(0),weight(8),name(NULL)//初始化列表
#include <iostream>
#include <string>
using namespace std;
class People {
private:
int age;
int weight;
char* name;
public:
People() :age(0),weight(8),name(NULL)//初始化列表
{
name = (char*)malloc(100);
strcpy(name, "123456");
}
};
int main() {
return 0;
}
拷贝构造函数
用途
通过一个对象赋值出另一个对象。拷贝构造函数是特殊的构造,主要是用一个已有对象整体初始化新的对象。
声明
class Student {
private:
int num;
int age;
public:
Student() {//构造函数
}
Student(const Student& ss) {//默认拷贝构造函数
this->num = ss. num;
this->age = ss.age;
}
int getNumber() {
return num;
}
};
调用时机
1、对象以传递的方式传入函数参数
Student p;
cout << p.getNumber() << endl;
Student p2(p);//p2是通过p拷贝构造出来的
cout << p2.getNumber() << endl;
2、对象以值传递的方式传入函数
void printStudent(Student s) {
cout << &s << " " << s.getNumber() << endl;
}
int main() {
Student p;
printStudent(p);
cout << &p << " " << s.getNumber() << endl;
return 0;
}
3、对象以值得传递方式从函数返回
Student getStudent() {
Student s;
return s;
}
int main() {
Student p = getStudent();
return 0;
}
浅拷贝(默认拷贝函数)
只对对象中的数据成员进行简单的赋值
Student(const Student& ss){//默认拷贝构造函数
this->num = ss.num;
this->age = ss.age;
}
深拷贝:防止默认拷贝发生
深拷贝原理
深拷贝指的就是当拷贝对象中有对其他资源(如堆、文件、系统等)的引用时(引用可以是指针或引用)时,对象的另开辟一块新的资源,而不再对拷贝对象中有对其他资源的引用的指针或引用进行单纯的赋值。
声明一个私有拷贝构造函数
#include <iostream>
#include <string>
using namespace std;
class Student {
private:
char* name;
public:
Student(const char* name) {
this->name = (char*)malloc(12);
strcpy(this->name, name);
}
~Student() {
free(name);
}
Student(const Student& ss) {//深拷贝
this->name = (char*)malloc(12);
strcpy(this->name, ss.name);//拷贝构造赋值时另创一个空间
}
char* getName() {
return name;
}
};
int main() {
Student s("abc");
cout << s.getName() << endl;
Student s2(s);
cout << s.getName() << endl;
return 0;
}
浅拷贝和深拷贝区别
在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的;但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象,所以,此时,必须采用深拷贝。
深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。简而言之,当数据成员中有指针时,必须要用深拷贝。
防止使用拷贝构造函数
私有化拷贝构造函数
private:
Student(const Student& ss);
参数可以传对象么?
不可以,这会自己调用自己,陷入死循环,得用引用
继承
1、单继承
1、继承过来的成员,可以通过子类对象访问
继承方式/基类成员 | public成员 | protected成员 | private成员 |
---|---|---|---|
public继承 | public | protected | 不可见 |
protected继承 | protected | protected | 不可见 |
private继承 | private | private | 不可见 |
#include<iostream>
#include<string.h>
using namespace std;
class People {
protected:
int age;
int weight;
char* name;
};
class Student :public People {
private:
int number;
public:
int getAge() {
return age;
}
};
int main() {
People s;
return 0;
}
2、函数隐藏:局部大于全局
如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名(只要函数名相同),那么就会隐藏从基类继承过来的成员,就是在派生类中使用该成员,实际上使用的是派生类新增的成员,而不是从基类继承来的
#include<iostream>
#include<string.h>
using namespace std;
class People {
protected:
int age;
int weight;
char* name;
public:
int getAge() {
cout << "People" << endl;
return age;
}
};
class Student :public People {
private:
int number;
public:
int getAge() {
cout << "Student" << endl;
return age;
}
};
int main() {
Student s;
s.getAge();
return 0;
}
//输出:Student
使用基类的成员函数格式:对象.基类::函数()
int main() {
Student s;
s.People::getAge();
return 0;
}
//输出:People
3、基类,派生类的构造析构顺序(栈)
基类,派生类的构造析构顺序遵循栈的特点,先进后出
父->子->子->父
爷->父->孙->父->爷
2、多重继承:子类继承父类
class People {
};
class Student :public People {
};
class CollegeStudent :public Student {//多重继承
};
4、多继承
#include<iostream>
#include<string.h>
using namespace std;
class People {
protected:
int age;
public:
void doWork() {
cout << "吃饭 睡觉 打豆豆" << endl;
}
};
class Worker :public People {
public:
void doWork2() {
cout << "搬砖" << endl;
}
};
class Famer:public People {
public:
void doWork3() {
cout << "种地" << endl;
}
};
class MigrantWorker : public Worker, public Famer {//多继承
};
int main() {
MigrantWorker m;
m.doWork2();//搬砖
m.doWork3();//种地
return 0;
}
3、虚继承
1、为什么存在?
现象:假如我们有类A是父类,类B和类C继承了类A,而类D既继承类B又继承类C(这种菱形继承关系)。当我们实例化D的对象的时候,每个D的实例化对象中都有了两份完全相同的A的数据。因为保留多分数据成员的拷贝,不仅占用较多的存储空间,还增加了访问这些成员是的困难,容易出错,而实际上,我们并不需要有多份拷贝。
2、解决方法
虚拟继承方法:在子类继承父类时加上关键字virtual
虚继承解决:访问不明确的问题。
#include<iostream>
#include<string.h>
using namespace std;
class People {
public:
int age;
public:
void doWork() {
cout << "吃饭 睡觉 打豆豆" << endl;
}
};
class Worker :virtual public People {
public:
void doWork2() {
cout << "搬砖" << endl;
}
};
class Farmer :virtual public People {
public:
void doWork3() {
cout << "种地" << endl;
}
};
class MigrantWorker :virtual public Worker, virtual public Farmer {
};
int main() {
MigrantWorker m;
m.age = 10;//虚继承解决访问不明确的问题。
return 0;
}
多态
1、多态的概念与分类
概念:多态字面意思为多种状态。在面向对象语言中,一个接口,多种实现即为多态。
分类:C++中的多态性具体体现在编译和运行两个阶段。
-
编译时多态是静态多态,在编译时就可以确定使用的接口。
-
运行时多态是动态多态,具体引用的接口在运行时才能确定。
静态多态和动态多态的区别:其实只是在什么时候将函数实现和函数调用关联起来,是在编译时期还是运行时期,即函数地址是早绑定还是晚绑定的。
- 静态多态(早绑定):是指在编译期间就可以确定函数的调用地址,并生产代码,这就是静态的。静态多态往往也被叫做静态联编。
- 动态多态(晚绑定):则是指函数调用的地址不能在编译器期间确定,需要在运行时确定。动态多态往往也被叫做动态联编。
2、多态的作用
封装可以使得代码模块化,继承可以扩展已存在的代码,他们的目的都是为了代码重用。而多态的目的则是为了接口重用。静态多态,将同一个接口进行不同的实现,根据传入不同的参数(个数或类型不同)调用不同的实现。动态多态,则不论传递过来的哪个类的对象,函数都能够通过同一个接口调用到各自对象实现的方法。
3、静态多态
静态多态往往通过函数重载和模版(泛型编程)来实现,具体可见下面代码:
#include <iostream>
using namespace std;
//两个函数构成重载
int add(int a, int b)
{
cout<<"in add_int_int()"<<endl;
return a + b;
}
double add(double a, double b)
{
cout<<"in add_double_doube()"<<endl;
return a + b;
}
//函数模板(泛型编程)
template <typename T>
T add(T a, T b)
{
cout<<"in func tempalte"<<endl;
return a + b;
}
int main()
{
cout<<add(1,1)<<endl; //调用int add(int a, int b)
cout<<add(1.1,1.1)<<endl; //调用double add(double a, double b)
cout<<add<char>('A',' ')<<endl; //调用模板函数,输出小写字母a
}
/*
in add_int_int()
2
in add_double_doube()
2.2
in func tempalte
a
*/
4、动态多态
动态多态最常见的用法就是声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而调用不同的方法。如果没有使用虚函数,即没有利用C++多态性,则利用基类指针调用相应函数的时候,将总被限制在基类函数本身,而无法调用到子类中被重写过的函数。因为没有多态性,函数调用的地址将是一定的,而固定的地址将始终调用同一个函数,这就无法“实现一个接口,多种实现”的目的了。
#include <iostream>
using namespace std;
class Base
{
public:
virtual void func()
{
cout << "Base::fun()" << endl;
}
};
class Derived : public Base
{
public:
virtual void func()
{
cout << "Derived::fun()" << endl;
}
};
int main()
{
Base* b=new Derived; //使用基类指针指向派生类对象
b->func(); //动态绑定派生类成员函数func
Base& rb=*(new Derived); //也可以使用引用指向派生类对象
rb.func();
}
/*
Derived::fun()
Derived::fun()
*/
通过上面的例子可以看出,在使用基类指针或引用指向子类对象时,调用的函数是子类中重写的函数,这样就实现了运行时函数地址的动态绑定,即动态联编。动态多态是通过“继承+虚函数”来实现的,只有在程序运行期间(非编译期)才能判断所引用对象的实际类型,根据其实际类型调用相应的方法。具体格式就是使用virtual关键字修饰类的成员函数时,指明该函数为虚函数,并且派生类需要重新实现该成员函数,编译器将实现动态绑定。
5、为什么父类指针可以指向子类?
可以通俗的理解,子类可能含有一些父类没有的成员变量或者方法函数,但是子类肯定继承了父类所有的成员变量和方法函数。
所以用父类指针指向子类时,没有问题,因为父类有的,子类都有,不会出现非法访问问题。但是如果用子类指针指向父类的话,一旦访问子类特有的方法函数或者成员变量,就会出现非法。
虽然父类指针可以指向子类,但是其访问范围还是仅仅局限于父类本身有的数据,那些子类的数据,父类指针是无法访问的。
6、动态多态实现原理
1、使用虚函数实现多态
C++中运行时多态可以通过声明一个虚函数来实现。虚函数分为纯虚方法和半虚方法,纯函数父类没有实现版本,完全交给子类,且必须实现。半虚函数父类可以实现,子类需要重写,他们都由关键字virtual修饰。
2、虚函数的实现原理
一个类中如果有虚函数声明,那么这些函数会由一个虚函数表来维护
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。
3) 内存布局中,其父类布局依次按声明顺序排列。
4) 每个父类的虚表中的函数都被overwrite成了子类函数。这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
虚函数
虚函数分为纯虚方法和半虚方法,纯函数父类没有实现版本,完全交给子类,且必须实现。半虚函数父类可以实现,子类需要重写,他们都由关键字virtual修饰。
1、以下函数不能作为虚函数
1)友元函数,它不是类的成员函数
2)全局函数
3)静态成员函数,它没有this指针
3)构造函数,拷贝构造函数,以及赋值运算符重载(可以但是一般不建议作为虚函数)
2、纯虚函数
提出原因:在基类中想定义虚函数(为了更好的试用与对象),但又不想给这个虚函数提出有意义的实现,这是就要用到纯虚函数。
1、纯虚函数特性
-
虚函数等于 0
-
这个类为抽象类,抽象类不允许实例化
#include<iostream>
using namespace std;
class People { //抽象类
public:
virtual void play() = 0;
};
class Student :public People {
public:
virtual void paly() { //如果子类不重写父类的纯虚函数,那么这个子类也是抽象类
cout << "打王者荣耀" << endl;
}
};
int main() {
People p;//不允许实例化
Student s;
return 0;
}
/*
打王者荣耀
*/
2、虚析构
1、使用场合
常常在基类中使用虚析构
2、虚析构函数的作用
虚析构函数使得在删除指向子类对象的基类指针时可以调用子类的析构函数达到释放子类中堆内存的目的,而防止内存泄露的
3、虚析构总结
- 如果父类的析构函数不加virtual关键字
当父类的析构函数不声明成虚析构函数的时候,当子类继承父类,父类的指针指向子类时,delete掉父类的指针,只调动父类的析构函数,而不调动子类的析构函数。 - 如果父类的析构函数加virtual关键字
当父类的析构函数声明成虚析构函数的时候,当子类继承父类,父类的指针指向子类时,delete掉父类的指针,先调动子类的析构函数,再调动父类的析构函数。
4、虚析构原理
-
先调用父类的构造函数,再调用子类的构造函数
这里有一个问题:父类的构造函数/析构函数与子类的构造函数/析构函数会形成多态,但是当父类的构造函数/析构函数即使被声明virtual,子类的构造/析构方法仍无法覆盖父类的构造方法和析构方法。这是由于父类的构造函数和析构函数是子类无法继承的,也就是说每一个类都有自己独有的构造函数和析构函数。
-
由于父类的析构函数为虚函数,所以子类会在所有属性的前面形成虚表,而虚表内部存储的就是父类的虚函数
-
当delete父类的指针时,由于子类的析构函数与父类的析构函数构成多态,所以得先调动子类的析构函数;之所以再调动父类的析构函数,是因为delete的机制所引起的,delete 父类指针所指的空间,要调用父类的析构函数。
5、为什么默认析构函数不是虚析构?
虚函数,会在类中产生一个虚指针(4字节),浪费空间。
#include<iostream>
#include<string.h>
using namespace std;
class People {//抽象类
public:
int age;
People() {
cout << "People" << endl;
}
virtual~People() {
cout << "~People" << endl;
}
};
class Student :public People {
public:
Student() {
cout << "Student" << endl;
}
virtual~Student() {
cout << "~Student" << endl;
}
};
int main() {
People* p = new Student;
delete p;
cout << sizeof(People) << endl;//8:int+一个虚指针
return 0;
}
/*
输出:
People
Student
~Student
~People
*/
3、虚指针
一个虚指针占4个字节。
虚指针与排在其他类型前面
4、虚函数列表
静态成员变量
1、静态成员变量
注意
- 静态成员变量是属于类的,不属于对象
- 静态成员变量,必须类外初始化
- 静态变量只初始化一次,结束于整个程序结束
#include<iostream>
using namespace std;
class People {
public:
int money = 10;
static int water;//属于类的,而不是属于对象的
};
//静态成员变量 必须类外初始化
int People::water = 100;
int main() {
People ss;
People mcy;
ss.water--;
cout << --ss.money << " " << mcy.water << " " << ss.water << endl;//9 99 99
People::water--;
cout << mcy.money << " " << mcy.water << " " << ss.water << endl;//10 98 98
return 0;
}
2、静态成员函数
注意
静态成员函数只能访问静态成员变量,否则会报错
#include<iostream>
using namespace std;
class People {
public:
int money = 10;
static int water;//属于类的,而不是属于对象的
static void drink() {//只能访问静态成员变量
water--;
//money--;报错
}
};
//静态成员变量 必须类外初始化
int People::water = 100;
int main() {
People ss;
People mcy;
ss.drink();
cout << --ss.money << " " << mcy.water
<< " " << ss.water << endl;//9 99 99
People::drink();
cout << mcy.money << " "
<< mcy.water << " " << ss.water << endl;//10 98 98
return 0;
}
内部类 内联函数 友元函数
1、内部类
在类中再定义一个类
#include<iostream>
using namespace std;
class People {
public:
class Baby {
public:
int age;
};
Baby b;
};
int main() {
People::Baby by;
by.age = 1;
cout << by.age << endl;
return 0;
}
2、内联函数
- 相当于宏函数
- 尽可能简单,运行效率相对高
- 使用方法:在函数前加 inline 即可
#include<iostream>
using namespace std;
class People {
int age;
public:
inline int geyAge() {//在函数前加 inline 即可
return age;
}
};
int main() {
return 0;
}
3、友元函数
- 可以访问私有成员
- 使用方法: 在需要友元的函数前加 friend
#include<iostream>
using namespace std;
class People {
int age = 28;
friend void printAge(People &p);
friend int main();
};
void printAge(People &p) {
cout << p.age << endl;
}
int main() {
People p;
printAge(p);
p.age = 29;
return 0;
}
- 友元类:这个类可以访问他的私有属性
#include<iostream>
using namespace std;
class People {
int age = 28;
friend class Animal;
};
class Animal {
public:
void printAge(People& p) {
cout << p.age << endl;
}
};
int main() {
People p;
Animal a;
a.printAge(p);//28
return 0;
}
4、常函数(const)
- 常函数不可以修改类的成员变量
#include<iostream>
using namespace std;
class People {
int age = 28;
int getAge() const {//不可以修改类成员变量
//age = 10;×
return age;
}
};
int main() {
const int a = 10;
//a=20;×
int b = 10;
int c = 30;
const int* p = &a;
//*p = 30;×
//p = &b; √
int const* pb = &a;
//*pb = 30;×
//pb = &b; √
int *const pc = &b;
//pc = &c;×
//*pc = 1;√
return 0;
}
5、缺省函数参数
#include<iostream>
using namespace std;
void add(int a, int b, int c = 2) {
cout << a + b + c << endl;
}
int main() {
add(1, 2);//5
return 0;
}
重载操作符
1、重载操作符
#include<iostream>
using namespace std;
class People {
public:
int age;
public:
People(int a) {
age = a;
}
/*People(const char* str) {
}*/
People() {//默认产生
age = 1;
}
//=运算符重载(默认产生)
People& operator=(const People& ss) {
this->age = ss.age;
return *this;
}
void operator=(const char* str) {
cout << str << endl;
}
void operator+=(const People& ss) {
this->age += ss.age;
}
int& operator++(){// ++i实现代码为:
*this += 1;
return *this;
}//返回一个int型的对象引用
int operator++(int){//i++实现代码为:
int temp = *this;
++*this;
return temp;
}//返回一个int型的对象本身
};
void operator<<(People& ss, int n) {//类外重载
ss.age = n;
}
int main() {
int a = 10;
int b;
int c;
c = b = a;
b += a;
a++;
++a;
People p1 = 10;//重载构造函数
People p2;
People p3;
p2.operator=(p1);//===> p2 = p1;
p3 = p2.operator = (p1);//p3 = p2 = p1;
p3 = 20;
p3 = "你好";
p2 += p1;
p1++;
++p2;
p2 << 2;
cout << p2.age << endl;
return 0;
}
2、不允许重载的运算符
.
*
::
sizeof
?:(三目运算符)
3、必须放在类内重载的运算符
()
[]
->
=
4、i++和++i重载操作符
int& operator++(){// ++i实现代码为:
*this += 1;
return *this;
}//返回一个int型的对象引用
int operator++(int){//i++实现代码为:
int temp = *this;
++*this;
return temp;
}//返回一个int型的对象本身
重写string类
1、string类
#include<iostream>
#include<string>
using namespace std;
int main() {
string str1 = "123";
cin >> str1;//123
cout << str1 << endl;//123
string str2 = str1;
cout << str2 << endl;//123
string str3;
str3 = str2 = str1;
cout << str3 << endl;//123
if (str2 == str3) {
cout << "相等" << endl;//相等
}
cout << str1.length() << endl;//6
str3 += str2;
str2 = str1 + str2;
cout << str2 << " " << str3 << endl;//123123 123123
cout << str1[2] << endl;//3
return 0;
}
2、重写string类
String.h文件
/*String.h文件*/
#pragma once
#ifndef _M_STRING_H_
#define _M_STRING_H_
#include<iostream>
class String {
public:
//const char *str = 0 缺省函数参数
String(const char *str = 0);//通用构造函数
String(const String &another);//拷贝构造函数
~String();//析构函数
String& operator=(const String &ss);//赋值函数
char operator[](int index);
private:
char *m_data;//用于保存字符串
friend std::ostream& operator<<(std::ostream& os,String& str);
};
#endif
String.cpp文件
/*String.cpp文件*/
#include"String.h"
#include<string.h>
String::String(const char *str) {
if (str == 0) {
m_data = new char(0);
}
else {
m_data = new char[strlen(str) + 1];
strcpy(m_data, str);
}
}
String::String(const String &another) {
m_data = new char[strlen(another.m_data) + 1];
strcpy(m_data, another.m_data);
}
String::~String() {
delete[]m_data;
}
String& String::operator=(const String &ss) {
delete[]m_data;
m_data = new char[strlen(ss.m_data) + 1];
strcpy(m_data, ss.m_data);
return *this;
}
char String::operator[](int index) {
//判断一下
return m_data[index];
}
std::ostream& operator<< (std::ostream& os, String& str) {
os << str.m_data;
return os;
}
main.cpp
#include<iostream>
#include"String.h"
#include<string>
using namespace std;
int main() {
string str1 = "abcdef";
String m_str1 = "abcdef";
string str2;
String m_str2;
//走拷贝构造函数
string str3 = str1;//string str3(str1);
String m_str3 = m_str1;//String m_str3(m_str1);
//走重载操作符
str3 = str2;
m_str3 = m_str2;
cout << str1[2] << endl;
cout << m_str1[2] << endl;
cout << str1 << endl;
cout << m_str1 << endl;
return 0;
}
模板
1、模板函数
#include<iostream>
#include<string>
using namespace std;
//模板函数
template<class TypeA,class TypeB>
void add(TypeA a, TypeB b) {
cout << a + b << endl;
}
int main() {
add(10, 20);
add(10, 20.1);
add(10.1, 20.2);
add(string("add"), string("1234"));
return 0;
}
/*
30
30.1
30.3
add1234
*/
2、模板类
#include<iostream>
#include<string>
using namespace std;
//模板类
template<class Type>
class People {
Type val;
public:
People(Type v) {
val = v;
}
};
int main() {
People<int> p(10);
People<double> p2(10.1);
People<string> p3("abcd");
return 0;
}
C和C++的区别是什么
- C是面向过程的语言,而C++是面向对象的语言,C只能写面向过程的代码,而C++既可以写面向过程的代码,也可以实现面向对象的代码
- C和C++一个典型的区别就在动态内存管理上了,C语言通过malloc和free来进行堆内存的分配和释放,而C++是通过new和delete来管理堆内存的
- 强制类型转换上也不一样:C的强制类型转换使用()小括号里面加类型进行类型强转的,C++有四种自己的类型强转方式,分别是const_cast,static_cast,reinterpret_cast和dynamic_cast
- C和C++的输入输出方式也不一样,printf/scanf,和C++的cout/cin的对别,前面一组是C的库函数,后面是ostream和istream类型的对象。
- C++还支持带有默认值的函数,函数的重载,inline内联函数,这些C语言都不支持
- 由于C++是面向对象的语言,支持类对象,类和类之间的代理,组合,继承,多态等等面向对象的设计,有很多的设计模式可以直接使用,因此在设计大型软件的时候,通常都会采用面向对象语言,而不会采用面向过程语言,可以更好的进行模块化设计,做到软件设计的准则:高内聚,低耦合!
- 在C++中,struct关键字不仅可以用来定义结构体,它也可以用来定义类
C++ class和struct到底有什么区别
C语言中,struct 只能包含成员变量,不能包含成员函数。
C++中的 struct 和 class 基本是通用的,唯有几个细节不同:
- 使用 class 时,类中的成员默认都是 private 属性的;而使用 struct 时,结构体中的成员默认都是 public 属性的。
- class 继承默认是 private 继承,而 struct 继承默认是 public 继承。
- class 可以使用模板,而 struct 不能。
C++内联函数(inline)的工作原理
内联函数(inline function与一般的函数不同,它不是在调用时发生控制转移,而是在编译阶段将函数体嵌入到每一个调用该函数的语句块中。 内联函数(inline function) 与编译器的工作息息相关 。编译器会将程序中出现内联函数的 调用表达式用内联函数的函数体来替换。
内联函数的优点:
被调用函数是将程序执行转移到被调用函数所存放的内存地址,将函数执行完后,在返回到执行此函数前的地方。这种转移操作需要保护现场、包括进栈等操作,在被调用函数代码执行完后,再恢复现场。但是保护现场和恢复现场需要较大的资源开销。对于一些较小的调用函数来说,若是频繁调用,函数调用过程甚至可能比函数执行过程需要的系统资源更多。所以引入内联函数,可以让程序执行效率更高。
内联函数的缺点:
如果调用内联函数的地方过多,也可能造成代码膨胀。因为编译器会把内联函数的函数体嵌入到每一个调用了它的地方,重复地嵌入。
内存管理
C++中,内存分为5个区,它们分别是堆,栈,自由存储区,全局/静态存储区和常量存储区
栈
在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配内存容量有限。
堆
由 new 分配的内存块,他们的释放编辑器不去管,有我们的应用程序去控制,一般一个 new 要对应一个 delete。如果程序员没有释放掉,那么程序结束后,os会自动回收。
自由存储区
由 malloc 分配的内存块,和堆十分相似,不过一个 malloc 对应一个 free
全局/静态存储区
全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化和未初始化的,在C++里面没有这个分区了,他们共同占用同一块内存区。
常量存储区
这里存放的常量,不允许被修改
说说你对于static关键字的理解 ?
-
static是静态修饰符,可以修饰成员
-
被static修饰的成员属于类而不属于对象 static修饰内容随着类的存在而存在 优先对象
-
static修饰的内容可以使用类名.的方式访问,无需创建对象
-
静态方法不可以直接调用非静态成员
static和const的区别
1、static
static定义的变量在程序初始化的时候会初始化在静态数据区,程序运行期间完全不变,是指这个区域不变,而内容是可以改变的。换句话说,static只是指定将其存储在静态存储区。不同于一般变量,一般变量在运行时候由程序分配内存,而静态变量编译的时候就将分配内存。
比如你的问题中,a将被赋值为3,然后,如果你在程序中写a=a+1;a就会变成4,静态变量和一般变量唯一的不同就是整个程序运行期间都将存在。它所在的内存不会被释放。
作用
static最重要的作用是控制元素的作用域。被static的变量或者方法,将是只有本文件可见的!
2、const(右值)
const关键字定义的是只读变量,一经赋值,再也不能改变了。
3、区别
我有两个文件 a.c和b.c,其中a.c中是这样的:
int _a = 1; // 这是一个全局变量
void fun() // 这是一个全局函数
{
...
}
这时候在b.c中是可以调用 _a和fun()的,但是如果我加入了在_a或者fun()之前加入了static,那么两者均只能在a.c中被调用。
好处主要是两种:
1)不同文件的全文件变量或者函数可以采用同样的变量/函数名,方便编程,static起到了隔离文件的作用。
2)有些变量你想让他作为一个长期变量/函数,但是又不想让他成为全局变量/函数,static就有用了,因为static变量/函数跟全局变量/函数在初始化时同样是开辟在静态区的。
- 设计static时,能将初始值置为0,可用于对字节进行置0操作。
new 和 malloc
new 和 malloc 区别
1、new/delete 是C++运算符,需要编译器支持。malloc/free 是库函数,需要头文件支持
2、使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型计算自行计算。而malloc则需要显示的指出所需内存的尺寸。
3、new 操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象分配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void* ,需要通过强制类型转换将 void* 指针转换成我们需要的类型。
4、new 内存分配失败时,会抛出bac_alloc 异常。malloc 分配内存失败时返回 NULL。
5、new 会先调用 operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用 operator delete函数释放内存(通常底层使用free实现)。malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
int main() {
People* p = (People*)malloc(sizeof(People));
free(p);
People* b = new People;
delete b;
return 0;
}
有了malloc/free为什么还要new/delete
-
malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
-
对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
-
因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。
delete和delete[] 区别
1、注意
new申请的内存,释放时用delete,使用new [ ]申请的内存释放时要用delete [ ]
2、原因
1、基本数据类型
int *a = new int[10];
...
delete a; // 方式1
delete [ ] a; //方式2
针对简单的基本数据类型,方式1和方式2均可正常工作。
因为:基本的数据类型对象没有析构函数,并且new 在分配内存时会记录分配的空间大小,则delete时能正确释放内存,无需调用析构函数释放其余指针。因此两种方式均可。
2、自定义数据类型
这里一般指类,假设通过new申请了一个对象数组,注意是对象数组,返回一个指针,对于此对象数组的内存释放,需要做两件事情:一是释放最初申请的那部分空间,二是调用析构函数完成清理工作。对于内存空间的清理,由于申请时记录了其大小,因此无论使用delete还是delete[ ]都能将这片空间完整释放,而问题就出在析构函数的调用上,当使用delete时,仅仅调用了对象数组中第一个对象的析构函数,而使用delete [ ]的话,将会逐个调用析构函数。
实例
#include <iostream>;
using namespace std;
class T {
public:
T() { cout << "constructor" << endl; }
~T() { cout << "destructor" << endl; }
};
int main()
{
const int NUM = 3;
T* p1 = new T[NUM];
cout << hex << p1 << endl; //输出P1的地址
// delete[] p1;
delete p1;
cout << endl;
T* p2 = new T[NUM];
cout << p2 << endl; //输出P2的地址
delete[] p2;
return 0;
}
输出结果为
可以看到,不加[ ]符号时确实只调用了一次析构函数。
问题来了,既然不加方括号也能完整释放内存,那不就没多调用几个析构函数吗,怎么了?想想看,万一析构函数需要释放系统资源呢?比如文件?线程?端口?这些东西使用了而不释放将会造成严重的后果。因此,虽然内存完整的释放了,但是有时候不调用析构函数则会造成潜在的危险。