这次来分享一下最近复习回顾并且进行补充的知识点
内联函数
inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。
如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用。 不会建立栈帧。
查看方式:
-
在release模式下,查看编译器生成的汇编代码中是否存在call Add
-
在debug模式下,需要对编译器进行设置,否则不会展开
特性:
inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。
inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联。
inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
#include<iostream>
using namespace std;
inline int add(int x, int y)
{
return x + y;
}
int main()
{
printf("%d", add(1, 2));
return 0;
}
// F.h
#include <iostream>
using namespace std;
inline void f(int i);
// F.cpp
#include "F.h"
void f(int i)
{
cout << i << endl;
}
// main.cpp
#include "F.h"
int main()
{
f(10);
return 0;
}
// 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl f(int)" (?
//f@@YAXH@Z),该符号在函数 _main 中被引用
//内联函数不生成指令形成符号表,可以在.h文件中直接实现声明和定义同时,只是在调用的地方展开
//不会去call地址,类似于宏
//inline替代的是宏函数
类和对象---封装
【访问限定符说明】
-
public修饰的成员在类外可以直接被访问
-
protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
-
访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
-
class的默认访问权限为private,struct为public(因为struct要兼容C) 。
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
【面试题】 面向对象的三大特性:封装、继承、多态。 在类和对象阶段,我们只研究类的封装特性,那什么是封装呢?
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。 封装本质上是一种管理:我们如何管理兵马俑呢?比如如果什么都不管,兵马俑就被随意破坏了。那么我们首先建了一座房子把兵马俑给封装起来。但是我们目的全封装起来,不让别人看。所以我们开放了售票通道,可以买票突破封装在合理的监管机制下进去参观。类也是一样,我们使用类数据和方法都封装到一下。不想给别人看到的,我们使用protected/private把成员封装起来。开放一些共有的成员函数对成员合理的访问。所以封装本质是一种管理。
封装意义一:
在设计类的时候,属性和行为写在一起,表示事物
语法:
class 类名{访问权限:属性/行为}
示例一:设计一个圆类,求圆的周长
#include<iostream>
using namespace std;
//涉及一个圆类,求圆的周长
//圆求周长的公式:2*PI*半径
const double PI = 3.14;
//class 代表设计一个类,类后面紧跟着的就是类的名称
class Circle
{
//访问权限
//公共权限
public:
//属性
//半径
int m_r;
//行为
//获取圆的周长
double calculateZC()
{
return 2 * PI * m_r;
}
};
int main()
{
//通过圆类,创建具体的圆(对象)
//实例化(通过一个类创建一个对象的过程)
Circle c1;
//给圆对象的属性进行赋值
c1.m_r = 10;
//2 * PI * 10 = 62.8
cout << "圆的周长为:" << c1.calculateZC() << endl;
return 0;
}
设计一个学生类,属性有姓名和学号,可以给姓名和学号赋值,可以显示学生的姓名和学号
#include<iostream>
#include<string.h>
using namespace std;
class Student
{
public:
//类中的属性和行为,我们统一称为成员
//属性 成员属性 成员变量
//行为 成员函数 成员方法
string m_name; //姓名
int m_ID; //学号
void showStudent()
{
cout << "姓名: " << m_name << " 学号:" << m_ID << endl;
}
void setName(string name)
{
m_name = name;
}
void setID(int id)
{
m_ID = id;
}
};
int main()
{
//创建一个学生
Student s1;
Student s2;
s1.m_name = "金旭";
s1.m_ID = 1;
s2.setName("张三");
s2.setID(2);
s1.showStudent();
s2.showStudent();
return 0;
}
封装意义二:
类在设计时,可以把属性和行为放在不同的权限下,加以控制
访问权限有三种:
1.public 公共权限
2.protected 保护权限
3.private 私有权限
示例:
访问权限:
1.public 公共权限 成员类内可以访问,类外可以访问
2.protected 保护权限 类内可以访问,类外不可以访问 儿子可以访问父亲中的保护内容
3.private 私有权限 类内可以访问,类外不可以访问 儿子不可以访问父亲的私有内容
#include<iostream>
using namespace std;
class Person
{
public:
//公共权限
string name;//姓名
protected:
//保护权限
string car;//汽车
private:
//私有权限
int password;//银行卡密码
//类内
public://private,protested都可以,但类外访问不到该函数
void func()
{
name = "张三";
car = "拖拉机";
password = 123456;
}
//类内可自由访问
};
int main()
{
//实例化具体对象
Person p1;
p1.name = "李四";
//p1.car = "奔驰"; 报错,声明不可访问,保护权限在类外访问不到,私有权限同
return 0;
}
类的作用域:
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
cout<<_name<<" "_gender<<" "<<_age<<endl;
}
//C++栈的模拟实现
#include<iostream>
using namespace std;
class Stack
{
public:
void Init();
void Push(int x);
int Top();
private:
int* a;
int top;
int capacity;
};
#include"Stack.h"
void Stack::Init()
{
a = nullptr;;
top = capacity = 0;
}
void Stack::Push(int x)
{
if (top == capacity)
{
size_t newcapacity = capacity == 0 ? 4 : capacity * 2;
a = (int*)realloc(a, sizeof(int) * newcapacity);
if (a == NULL)
{
exit(-1);
}
capacity = newcapacity;
}
a[top++] = x;
}
int Stack::Top()
{
return a[top - 1];
}
#include"Stack.h"
int main()
{
Stack st;
st.Init();
st.Push(1);
st.Push(2);
st.Push(3);
st.Push(4);
//cout << st.a[st.top - 1] << endl; 不可访问,如果设置可访问易出错
cout << st.Top() << endl;
return 0;
}
类的实例化:
用类类型创建对象的过程,称为类的实例化
-
类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它
-
一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量
-
做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间
#include<iostream>
using namespace std;
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Init(2023, 7, 19);
d1.Print();
Date d2;
d2.Init(2023, 7, 20);
d2.Print();
return 0;
}
类对象模型:
如何计算类对象的大小:
#include<iostream>
using namespace std;
class A
{
public:
void printA()
{
cout << m_a << endl;
}
private:
int m_a;
};
int main()
{
return 0;
}//内存中占有4个字节
// 类中既有成员变量,又有成员函数
class A1 {
public:
void f1(){}
private:
int _a;
};
// 类中仅有成员函数
class A2 {
public:
void f2() {
}
};
// 类中什么都没有---空类
class A3
{
//...
};
//sizeof(A1) : ___4___ sizeof(A2) : ___1___ sizeof(A3) : ___1___
//成员函数不占用内存空间,只有成员变量占用空间
//但一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐,注意空类的大小
//空类比较特殊,编译器给了空类一个字节来唯一标识这个类
结构体内存对齐规则
1.第一个成员在与结构体偏移量为0的地址处。
其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS中默认的对齐数为8
3.结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是 所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
struct和class的区别
#include<iostream>
using namespace std;
class C1
{
int A;//默认权限是私有权限
};
struct C2
{
int A;//默认权限是公共权限
};
int main()
{
C1 c1;
//c1.A = 100 不可访问
C2 c2;
c2.A = 100;//可访问
return 0;
}
在C++中struct 和 class 的唯一区别就在于默认的访问权限不同
区别:
struct默认权限为公共
class 默认权限为私有
成员属性设置为私有
优点1:将所有成员属性设置为私有,可以自己控制读写权限
优点2:对于写权限,我们可以检测数据的有效性
示例:
设计案例:
#include<iostream>
using namespace std;
class Cube
{
public:
//设置长
void setL(int L)
{
m_L = L;
}
//获取长
int getL()
{
return m_L;
}
//设置宽
void setW(int W)
{
m_W = W;
}
//获取宽
int getW()
{
return m_W;
}
//设置高
void setH(int H)
{
m_H = H;
}
//获取高
int getH()
{
return m_H;
}
//获取体积
int getV()
{
return m_L * m_H * m_W;
}
//获取面积
int getS()
{
return 2 * (m_L * m_W + m_L * m_H + m_W * m_H);
}
bool isSameByClass(Cube& c)
{
if (m_L == c.getL() && m_W == c.getW() && m_H == c.getH())
{
return true;
}
return false;
}
private:
int m_L;//长
int m_W;//宽
int m_H;//高
};
//全局函数判断两个立方体是否相等
bool isSame(Cube& c1, Cube& c2)
{
if (c1.getL() == c2.getL() && c1.getW() == c2.getW() && c1.getH() == c2.getH())
{
return true;
}
return false;
}
int main()
{
Cube s1,s2,c;
s1.setL(1);
s1.setW(2);
s1.setH(3);
s2.setL(1);
s2.setW(2);
s2.setH(4);
c.setL(1);
c.setH(3);
c.setW(2);
bool ret = isSame(s1, s2);
if (ret)
{
cout << "s1 和 s2 是相等的" << endl;
}
else
{
cout << "s1 和 s2 是不相等的" << endl;
}
bool retbyclass = c.isSameByClass(c);
if (retbyclass)
{
cout << "成员函数判断:s1 和 c 是相等的" << endl;
}
else
{
cout << "成员函数判断:s1 和 c 是不相等的" << endl;
}
cout << "s1的体积为:" << s1.getV() << endl;
cout << "s1的面积为:" << s1.getS() << endl;
return 0;
}
对象的初始化和清理
C++中的面向对象来源于生活,每个对象都会有初始设置以及对象销毁前的清理数据的设置
如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数。
构造函数和析构函数
对象的初始化和清理也是两个非常重要的安全问题
一个对象或者变量没有初始状态,对其以后使用后果也是未知
同样的使用完一个对象或者变量,没有及时清理,也会造成一定的安全问题
C++利用了构造函数和析构函数解决上述问题,这两个函数会被编译器自动调用,完成对象初始化和清理工作
对象初始化和清理工作是编译器强制要求我们做的东西,因此如果我们不提供构造与析构,编译器会提供编译器提供的构造函数和析构函数是空实现
一、构造函数语法:类名(){}
-
构造函数没有返回值也不写void
-
函数名称与类名相同
-
构造函数可以有参数,因此可以发生重载
-
程序在调用对象时也会自动调用构造,无须手动调用,而且只会调用一次
class Date
{
public:
// 1.无参构造函数
Date ()
{
//...
}
// 2.带参构造函数
Date (int year, int month , int day )
{
_year = year ;
_month = month ;
_day = day ;
}
private :
int _year ;
int _month ;
int _day ;
};
void TestDate()
{
Date d1; // 调用无参构造函数
Date d2 (2015, 1, 1); // 调用带参的构造函数
// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
// 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
Date d3();
}
5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
class Date
{
public:
/*
// 如果用户显式定义了构造函数,编译器将不再生成
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
*/
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
// 将Date类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函
//数
// 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再
//生成
// 无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用
Date d1;
return 0;
}
-
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
-
关于编译器生成的默认成员函数,很多童鞋会有疑惑:在我们不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象year/month/_day,依旧是随机值。也就说在这里**编译器生成的默认构造函数并没有什么卵用??
解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语法已经定义好的类型:如int/char...,自定义类型就是我们使用class/struct/union自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数。
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;
//注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在
//类中声明时可以给默认值。
//比如 private:
// int _year = 1;
// int _month = 1;
// int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
小实例:
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date dt1;
dt1.Print();
Date dt2(2023,7,20);
dt2.Print();
return 0;
}
对于Date类,可以通过SetDate公有的方法给对象设置内容,但是如果每次创建对象都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢? 构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。
二、析构函数语法:~类名(){}
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作
-
析构函数,没有返回值并不写void
-
函数名称与类名相同,在名称前加上符号~
-
构造函数可以有参数,因此可以发生重载
-
程序在调用对象时候自动调用构造,无须手动调用,而且只会调用一次
#include<iostream>
#include<cstdlib>
using namespace std;
class Person
{
public:
Person()//初始化
{
cout << "构造函数 Person 被调用" << endl;//不调用的话系统中也会自己调用空实现的该函数
}
~Person()//清理释放
{
cout << "析构函数 ~Person被调用" << endl;//不调用的话系统中也会自己调用空实现的该函数
}
};
void test()
{
Person p;
}
int main()
{
test();
return 0;
}
5.下面的程序我们会看到,编译器生成的默认析构函数,对自定类型成员调用它的析构函数
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;
}
// 程序运行结束后输出:~Time()
// 在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?
// 因为:main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month,
// _day三个是
// 内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_t是Time类对
// 象,所以在
// d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。但是:
// main函数
// 中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date
// 类的析构函
// 数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部
// 调用Time
// 类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁
// main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析
// 构函数
// 注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数
6.如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如 Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类
三、C++模拟栈的实现(构造函数/析构函数)
#include<iostream>
#include<assert.h>
using namespace std;
//C++模拟栈的实现
class Stack
{
public:
Stack(size_t n = 4) //构造函数
{
cout << "Stack(size_t n = 4)" << endl;
if (n == 0)
{
a = nullptr;
top = capacity;
}
else
{
a = (int*)malloc(sizeof(int) * n);
if (a == nullptr)
{
perror("malloc fail");
exit(-1);
}
top = 0;
capacity = n;
}
}
~Stack()
{
free(a);
a = nullptr;
top = capacity = 0;
}
void Push(int x)
{
if (top == capacity)
{
int newcapacity = capacity == 0 ? 4 : capacity * 2;
int* tmp = (int*)realloc(a, sizeof(int) * newcapacity);
if (tmp == nullptr)
{
perror("realloc fail");
exit(-1);
}
a = tmp;
capacity = newcapacity;
}
a[top++] = x;
}
int Top()
{
return a[top - 1];
}
void Pop()
{
assert(top > 0);
--top;
}
bool Empty()
{
return top == 0;
}
private:
int* a;
int capacity;
int top;
};
int main()
{
Stack st;
st.Push(1);
st.Push(2);
st.Push(3);
st.Push(4);
while (!st.Empty())
{
cout << st.Top() << endl;;
st.Pop();
}
return 0;
}
以上便是简单的简单的语法规则了,详细的如深拷贝,初始化列表等等知识可以参考C++Primer