C++入门3+类和对象上
- 一.内联函数
- 1.宏函数的缺点
- 2.宏函数的优点
- 3.内联函数的语法
- 4.内联函数的优缺点
- 5.内联函数的使用条件
- 6.内联函数的展开
- 7.内联函数的一大注意事项
- 1.内联函数声明跟定义分离
- 2.内联函数声明跟定义分离的"奇怪"现象
- 二.C++11对于C++语法的补充
- 1.auto关键字
- 1.auto关键字可以自动推导类型
- 2.auto的局限性
- 1.auto不能做参数
- 2.auto不能用作返回值
- 3.auto不能用来定义数组
- 4.auto定义时必须初始化
- 2.范围for
- 3.nullptr
- 三.类和对象上
- 1.C++中的结构体对于C语言的优化
- 1.C++中结构体的定义优化
- 2.为什么C++要引入class关键字来定义类呢?
- 2.类的书写形式
- 1.类的访问限定符和封装
- 2.类的声明和定义分离
- 3.类的大小及类的实例化
- 1.类的大小
- 2.不含有任何成员变量的类的大小
- 3.类的实例化
- 4.this指针
- 1.this指针的引出
- 2.被编译器隐藏的this指针
- 3.this指针的特性
- 1.this指针的两个经典题目
- 2.this指针的特性
- 5.C和C++当中关于Stack的实现对比
一.内联函数
C++针对于C语言的宏函数进行了优化,设计出了内联函数这一语法
其中在著名书籍《Effective C++》中,作者提出了一个很好的条款
他建议在C++程序中使用
enum 枚举常量,const 定义的常量来替代宏定义的常量
使用内联函数来替代宏函数
1.宏函数的缺点
关于这些情况的举例,大家可以看我的另一篇博客:
C语言预处理及宏和函数的区别与各自优劣点的详解
2.宏函数的优点
3.内联函数的语法
这里以Add函数为例
inline int Add(int a,int b)
{
return a+b;
}
只需要在函数前面加上关键字inline即可
可见语法非常简单
4.内联函数的优缺点
5.内联函数的使用条件
既然内联函数这么好,那么我们是不是就可以把所有的函数都设计成为内联函数呢?
当然是不行的,因为内联函数也是需要在调用的地方被展开的,只不过不是简单的宏的字符串替换而已
而且:
一般来说C++程序中10行以上的代码就无法成为内联函数
在《C++ primer》中关于inline的建议:
6.内联函数的展开
如果我们想看一下内联函数是怎么展开的呢?
这里使用的是VS2019,在默认配置下内联函数是不会展开的
那么怎么才能展开它呢?
在debug模式下,需要对编译器进行设置(因为debug模式下,编译器默认不会对代码进行优化)
右键找到属性
在调试信息格式这里修改为程序数据库这个选项
修改后:
在内联函数扩展这里改为只适用于_inline(/Ob1)
然后点击应用,确定
然后我们调试查看汇编代码
然后我们去掉inline,调试查看汇编代码
发现内联函数中没有call指令,也就是说inline函数并没有开辟栈帧,直接在原位置展开了
7.内联函数的一大注意事项
内联函数是不可以将声明跟定义分离的,为什么呢?
因为内联函数会直接在调用的地方展开,所以不需要去调用(汇编代码当中的call命令),
因此内联函数就不需要将地址存到符号表当中,因此在链接的时候通过符号表去查找函数的定义时便找不到内联函数的定义,因此会发生链接时错误
1.内联函数声明跟定义分离
下面给大家演示一下这个错误
我们定义了test.h,test.cpp,main.cpp并且生成解决方案:编译器爆出了链接时错误
那么请大家再看一下前面这段代码
2.内联函数声明跟定义分离的"奇怪"现象
这里为什么调用getf函数就可以成功执行f函数了呢?
因为getf函数是在test.cpp文件中定义的,也就是说getf函数的定义是跟f函数的定义放在同一个cpp文件中的,
因此getf函数想要寻找f函数根本不需要等到链接阶段(因为它们都被编译到了同一个.o目标文件中,而链接阶段是链接多个.o目标文件的阶段)
综上,内联函数声明跟定义分离的话,这个内联函数只能在它所定义的cpp文件中使用,在其他cpp文件中无法使用
因此内联函数不要声明跟定义分离
二.C++11对于C++语法的补充
1.auto关键字
1.auto关键字可以自动推导类型
//auto关键字
//可以自动推导类型
int main()
{
int a = 0;
int b = a;
auto c = a;
auto d = &a;
auto* e = &a;
auto& f = a;//f是a的别名,a是int,所以f也是int,因为f就是a
f++;
cout << typeid(c).name() << endl;//typeid可以打印对象的类型
cout << typeid(d).name() << endl;
cout << typeid(e).name() << endl;
cout << typeid(f).name() << endl;
//指针可以显式写,也可以隐式写
//但是引用只能显式写
/*
int
int *
int *
int
*/
return 0;
}
auto的真正有价值的用法:定义对象时,如果该对象的类型较长,用auto比较方便
//auto真正的用法:定义对象时如果类型较长,用它比较方便
#include <vector>
#include <string>
int main003()
{
vector<string> v;
vector<string>::iterator it = v.begin();
//简化写法:让这个类型定义的短一些,方便
auto it = v.begin();
return 0;
}
这里的vector容器和string容器都属于C++STL库中的知识,我们以后会进行重点介绍
iterator:迭代器,我们以后也会重点介绍
这里的vector容器就相当于数据结构中的顺序表,string就相当于数据结构中的字符串
2.auto的局限性
1.auto不能做参数
因为无法进行自动推导
可以使用模板来解决(关于模板的知识我们以后会进行重点介绍)
2.auto不能用作返回值
auto不能做返回值(新的规则可能支持了,但是VS中不支持,而且auto作为返回值的类型并不好,就像是python中的函数返回值类型一样)
因为调用函数时看不到函数的返回值,所以调用函数时很麻烦,还需要去看那个函数的源代码,太不方便了
因此就算C++引入了auto作为返回值,但是建议不要用auto作为函数的返回值
3.auto不能用来定义数组
4.auto定义时必须初始化
否则无法知道用auto定义的变量到底是什么类型
2.范围for
int main()
{
int arr[] = { 1,2,3,4,5 };
//一般情况下这里都用auto
//因为如果arr变为double,auto也不用改
//依次取数组中的数据赋值给e对象,自动判断结束,自动++往后走
for (auto e : arr)
{
cout << e << " ";
}
cout << endl;
for (int e : arr)
{
cout << e << " ";
}
cout << endl;
//修改数组中的每个数据
//需要加上&才能修改,这是指针所替代不了的
//因为指针赋值时需要取地址,而范围for是把数组中的数据进行赋值
for (auto& e : arr)
{
e *= 2;
}
for (auto e : arr)
{
cout << e << " ";
}
cout << endl;
/*
1 2 3 4 5
1 2 3 4 5
2 4 6 8 10
*/
return 0;
}
//这里arr传参时退化为指针,所以这里不能范围for
void testfor(int arr[])
{
for (auto e : arr)//err
{
cout << e << " ";
}
}
//二维数组呢?:也是指针,只不过是指向一维数组的指针
void testfor2(int arr[3][3])
{
//int (*)[3]:数组指针
//所以也不可以
for (auto e : arr)//err
{
cout << e << " ";
}
}
3.nullptr
在C++中NULL的定义跟在C语言中的定义不同
C语言中的: ((void*)0)
C++中的: 0
也就是说C语言中的NULL的确是空指针,而C++中的NULL却是字面常量0
大家看一下下面这种现象
请注意:函数的参数可以不要名字,只给一个类型
那这有什么意义呢?
我们在以后会学习运算符重载,而运算符重载中区分前置++和后置++时就需要用到这种参数
我们以后会重点讲解的
那么怎么修改这个错误呢?
C++11中新引入了一个关键字nullptr
这个nullptr就是(void*(0))
三.类和对象上
C++对于C语言中的结构体进行了优化,将结构体逐步演化成了类
又因为实际需求和C语言中的结构体的语法有所差异,(这个差异接下来会讲到)
所以引入一个关键字class来定义类
1.C++中的结构体对于C语言的优化
1.C++中结构体的定义优化
大家是不是在写C语言代码的时候都对一个现象感到很反感
比方说我要在C语言中定义一个单链表节点
struct SListNode
{
struct SListNode* next;
int val;
};
定义一个结构体变量必须要加struct关键字
struct SListNode* phead=NULL;
就算我们typedef起别名了
typedef struct SListNode
{
//这里还要加上struct.....
struct SListNode* next;
int val;
}SLNode;
定义一个结构体变量必须要加struct关键字
SLNode* phead=NULL;
必须要加这个struct是真的挺让人难受的,C++创始人也是这么想的,所以在C++语法中可以这么写
struct SListNode
{
//这里不需要加struct关键字
SListNode* next;
int val;
};
定义一个结构体变量不需要加struct关键字
SListNode* phead=nullptr;
第二个优化:C++中的结构体里面可以放函数
比方说我们要定义一个Stack栈
在C语言中我们只能这样定义
struct Stack
{
int* a;
int top;
int capacity;
};
void StackInit(struct Stack* ps)
{
ps->a = NULL;
ps->capacity = ps->top = 0;
}
void StackPush(struct Stack* ps, int x)
{
//扩容
//ps->a[ps->top++] = x;
}
而在C++中我们可以这么定义
请注意在C++中结构体,类都属于一个域,在不同的域中可以有重名函数
而且C++中的结构体中的函数访问结构体中的成员变量时可以不加结构体指针直接访问(其实是编译器帮助我们省略了this指针,这个我们在这篇博客最后会介绍this指针)
//C++中结构体
struct Stack1
{
int* a;
int top;
int capacity;
//这里的Init和Push可以不用加上结构体指针
//因为类也是一个域,只有在同一个域中才会构成重载
//类中的函数可以访问该类中的成员
void Init()
{
a = nullptr;
capacity = top = 0;
}
void Push(int x)
{
//扩容
//a[top++] = x;
}
};
C++中的结构体因此被称为类
不过C++中的结构体依然保留了C语言中结构体的用法,因为C++是兼容C语言的
而在C语言当中结构体中的成员变量是可以通过结构体类型的变量去访问的
也就是说C语言中的结构体中的成员变量是公有的
那么就会发生一个不太好的现象
2.为什么C++要引入class关键字来定义类呢?
首先要说明C++中的struct也是可以定义类的
只不过用class和用struct定义的类有一些区别
什么区别呢?
struct中的结构体的属性默认是公有的,也就是说结构体类型的成员变量是可以直接访问该结构体(类)的属性的
而class定义的类的属性默认是私有的,也就是说该结构体类型的成员变量是不可以访问该结构体的属性的
那么为什么要这么做呢?公有有什么不好的地方呢?私有有什么好的地方呢?
如果公有了,可能会发生下面这种现象:
int main()
{
Stack st1;
st1.Init();
//比方说我们要判断这个栈是否为空
//正常来说我们就是要调用Empty这个函数接口来判断这个栈是否为空
if (st1.Empty())
{
//一顿操作
}
else
{
//一顿操作
}
//可是,如果这个程序员的代码素养并不好,那么他可能会想
//你不就是想要判空吗,还需要调个函数,我直接看一下top等不等于0不就行了吗
//那么他可能会写出这样的代码
if (st1.top == 0)
{
//一顿操作
}
else
{
//一顿操作
}
//但是这个自作聪明的程序员忽视了一点,你定义栈的时候喜欢让top初始化为0,
//那别人就是喜欢初始化为-1也没任何错误啊
//你怎么知道这个栈的定义者定义的时候把top初始化为0还是-1呢?
//你不知道啊
//那么你还自作聪明的以你的想法去判断栈是否为空,这是一个不一定正确的想法
//可能就会产生bug,而这种bug产生的原因就是你这个程序员的代码素养不好
//因此C++创始人就想去限制你,强迫你去调用对应的接口来执行你想完成的操作
//怎么限制呢,就是让你访问不到top,只能借助Empty这个函数来判空
return 0;
}
也就是说:
C语言中代码的规范性极大程度上取决于程序员个人的代码素养
而我们的C++创始人针对这个问题
想要强制程序员:强制调用接口对数据成员进行操作从而规范代码
因此C++引入了class这个关键字
class定义的类默认是私有,但是函数也私有了,这怎么办?
这就要介绍一下类的访问限定符和封装的知识了
2.类的书写形式
1.类的访问限定符和封装
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户来使用
一般情况下都用class定义类,只有极少数情况下喜欢用struct
所以可这样来定义一个类
class Stack
{
private:
int* a;
int top;
int capacity;
public:
//这里的Init和Push可以不用
//因为类也是一个域,只有在同一个域中才会构成重载
//类中的函数可以访问该类中的成员
void Init()
{
a = nullptr;
capacity = top = 0;
}
void Push(int x)
{
//扩容
//a[top++] = x;
}
bool Empty()
{
return top == 0;
}
};
2.类的声明和定义分离
我们知道C语言中再写大型项目时经常会分头文件和源文件进行声明和定义分离
那么在C++的类中也可以这样
下面以栈为例
跟C语言有区别的地方在于在源文件中定义函数时需要加上类的域访问限定符
例如这里的
void Stack3::Init()
{
//具体实现
}
有一点需要特别说明:
C++也支持某些函数直接在类里面定义,而另一些函数进行声明和定义分离
但是有一个默认规则:直接在类中定义的函数默认加上inline的修饰
C++中类的推荐写法是:
长的函数:声明和定义分离来写
短的函数:直接在类中定义,默认会加上inline修饰
3.类的大小及类的实例化
1.类的大小
类作为一个数据类型,就必然会有它在内存中所对应的大小,
而类跟结构体类似,但是类中还有成员函数,那么这个类的大小是多少呢?
这里类C的大小是8个字节
根据我们在C语言阶段学过的结构体内存对齐的知识,我们可以计算出如果这个类只有成员变量_c和_i,那么这个结构体的内存大小就是8
那么成员函数不占用类的空间吗?
答案是:不占用
为什么呢?
因为:类C所定义出的每一个对象都有只属于它的一份数据,这一份数据是唯一的,是与其他人的数据互相独立的数据,就像是我们每一个人的身份证号都不同,每个人的年龄,性别,姓名等等都是只属于我们自己的数据,他人数据的变化对我们没有任何影响
所以类必须包含这些成员变量
而成员函数就像是一些公共设施,比如:公园,图书馆等等,不属于我们任何人
但是我们任何人都有权利去使用这些公共设施,如果这些公共设施只属于我们每个人,也就是每一个人都有一份公园,图书馆等等,那是不可能的,那也是非常消耗社会资源的一种分配方式
在这里也是同一个道理,这些成员函数并不是每个人所必需的,是可以为所有人所共同享用的,因此这些成员函数并不需要包含在对应的类当中,而是存储在了公共代码区
2.不含有任何成员变量的类的大小
那么如果一个类不含有任何成员变量呢?
无成员变量的类的大小:1
为什么呢?
这是规定的,这1个字节并不存储有效数据,就是为了表示定义的对象存在过
但是这个无成员变量的类也是有意义的
例如后面讲的仿函数都是不定义成员变量的
如果这个类的大小为0的话,那么实例化出来的对象岂不是0字节,也就是这个对象连地址都没有,也就是根本就不存在,这就跟实例化冲突了
3.类的实例化
那么什么是类的实例化呢?
说白了,就是定义一个类Date类型的变量,(注意C++中类定义变量可以称为对象,一回事,不过更习惯称之为对象)
int main()
{
Date d;//这就是类的实例化
return 0;
}
类和对象就类似于设计图和房子
只有在建造了房子之后,这个设计图中的桌子,床等等才会有
4.this指针
1.this指针的引出
我们在定义一个类的时候可能会出现下面这种情况:
我们明明初始化了,为什么还会这样?
因为Date类中的成员变量跟Init初始化函数的形参重名了,编译器会就近认为
这里的year=year的赋值是形参给形参自身赋值,所以并没有改变成员变量
那么怎么办呢?
第一种解决方法:
用简称代替形参,不过并不是很好,因为形参名称不写全了可能会产生歧义,而且需要去猜测形参的含义会加大程序员的工作难度,影响项目开发
第二种解决方法:
成员变量加上一些修饰符:
例如
在前面或后面加上_
或者在前面加上m_(m:member成员的意思)
或者m第一个首字母大写(驼峰法命名)
等等等等
第三种解决方法:this指针
2.被编译器隐藏的this指针
然后我们回头看看C语言和C++中对于Stack类的定义
可以发现它们的定义其实是一样的,只不过C++省略了this指针,把this指针隐藏了
编译器帮我们添加了this指针
那么为什么会报错呢?
是因为我们在形参列表中把this指针给显式定义了,其实编译器想告诉我们的是:
你不需要管this指针的事情,我会帮你做好的,你只需要管怎么去定义和使用函数即可
你要是管了,那就本末倒置了,你所要做的不是去为this指针的事情操心,而是去好好地写好你的函数,调用好你的函数就可以了
其实这里的
d1.Print()
的本质就是Print(&d1);
同理:
Date* dp=&d1;
dp->Print();
的本质就是:Print(dp);
3.this指针的特性
1.this指针的两个经典题目
下面请大家看一下这个代码,做一下这个选择题
1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
答案是C,为什么呢?
首先这个代码没有任何语法问题,编译阶段不会对空指针解引用这个行为进行检查的
p不是空指针吗,为什么还能正常运行呢?
因为Print()函数中并没有对this指针进行解引用
没有对空指针进行解引用,因此运行时便不会报错,因此正常运行
那么再请大家看一下这个代码结果是什么呢?
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
return 0;
}
首先,这个语法是没有问题的,编译阶段不会报错
但是Print函数当中出现了对空指针的解引用行为,因此这个代码在运行时会报错
2.this指针的特性
this指针被const修饰:
this指针不可以被赋值(即this指针的指向不可以被修改)
但是*this可以被赋值(即this指针所指向的对象的值可以被修改)
void Print(Date* const this)
还有一点:
this指针不能显式地传实参和形参
但是可以在类里面显式地使用
为什么支持显式地使用呢?
因为后面有一些情况下需要显式地写(有些情况下必须要使用this指针)
在上面的Date类中
void Print()
{
cout << this->_year << " " << this->_month << " " << this->_day << endl;
}
这么定义Print函数也是可以的
说了这么多,这么麻烦,C++有什么厉害的呢?
类和对象有什么厉害的?
下面给大家表演一下
5.C和C++当中关于Stack的实现对比
这是C语言的实现
#include <assert.h>
typedef int DataType;
typedef struct Stack
{
DataType* a;
int capacity;
int top;
}Stack;
void StackInit(Stack* ps)
{
assert(ps);
ps->a = (DataType*)malloc(sizeof(DataType) * 4);
if (NULL == ps->a)
{
assert(0);
return;
}
ps->capacity = 4;
ps->top = 0;
}
void StackDestroy(Stack* ps)
{
assert(ps);
if (ps->a)
{
free(ps->a);
ps->a = NULL;
ps->capacity = 0;
ps->top = 0;
}
}
void CheckCapacity(Stack* ps)
{
if (ps->top == ps->capacity)
{
int newcapacity = ps->capacity * 2;
DataType* temp = (DataType*)realloc(ps->a,newcapacity * sizeof(DataType));
if (temp == NULL)
{
perror("realloc申请空间失败!!!");
return;
}
ps->a = temp;
ps->capacity = newcapacity;
}
}
void StackPush(Stack* ps, DataType data)
{
assert(ps);
CheckCapacity(ps);
ps->a[ps->top] = data;
ps->top++;
}
int StackEmpty(Stack* ps)
{
assert(ps);
return 0 == ps->top;
}
void StackPop(Stack* ps)
{
if (StackEmpty(ps))
return;
ps->top--;
}
DataType StackTop(Stack* ps)
{
assert(!StackEmpty(ps));
return ps->a[ps->top - 1];
}
int StackSize(Stack* ps)
{
assert(ps);
return ps->top;
}
int main()
{
Stack s;
StackInit(&s);
StackPush(&s, 1);
StackPush(&s, 2);
StackPush(&s, 3);
StackPush(&s, 4);
printf("%d\n", StackTop(&s));
printf("%d\n", StackSize(&s));
StackPop(&s);
StackPop(&s);
printf("%d\n", StackTop(&s));
printf("%d\n", StackSize(&s));
StackDestroy(&s);
return 0;
}
下面是C++的实现
typedef int DataType;
class Stack
{
public:
void Init()
{
_a = (DataType*)malloc(sizeof(DataType) * 4);
if (NULL == _a)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = 4;
_top = 0;
}
void Push(DataType data)
{
CheckCapacity();
_a[_top] = data;
_top++;
}
void Pop()
{
if (Empty())
return;
_top--;
}
DataType Top()
{
return _a[_top - 1];
}
int Empty()
{
return 0 == _top;
}
int Size()
{
return _top;
}
void Destroy()
{
if (_a)
{
free(_a);
_a = NULL;
_capacity = 0;
_top = 0;
}
}
private:
void CheckCapacity()
{
if (_top == _capacity)
{
int newcapacity = _capacity * 2;
DataType* temp = (DataType*)realloc(_a, newcapacity * sizeof(DataType));
if (temp == NULL)
{
perror("realloc申请空间失败!!!");
return;
}
_a = temp;
_capacity = newcapacity;
}
}
private:
DataType* _a;
int _capacity;
int _top;
};
int main()
{
Stack s;
s.Init();
s.Push(1);
s.Push(2);
s.Push(3);
s.Push(4);
printf("%d\n", s.Top());
printf("%d\n", s.Size());
s.Pop();
s.Pop();
printf("%d\n", s.Top());
printf("%d\n", s.Size());
s.Destroy();
return 0;
}
折起来之后C++只有一个class
而C语言有一堆函数,还有一堆形参,而且传参的时候比起C++来很麻烦
怎么样,C++比C语言简洁吧.
为什么会这样呢?这一切都要多亏了类和对象的设计理念
还有编译器为我们隐藏的this指针
以上就是C++入门3和类和对象上的全部内容,希望对大家有所帮助!