c++入门语法收尾,类与对象(上)
1.引用与指针大不同点
代码演示:
int main()
{
int a = 10;
int& ra = a;
ra = 20;
cout << ra << endl;
return 0;
}
#include<iostream>
using namespace std;
int main()
{
int a = 10;
int* pa = &a;
*pa = 20;
return 0;
}
图片演示:
a都是一块空间的名字,ra是a的别名,所以也是这块空间的名字
这块空间的地址是0x00001000,指针pa是用来装0x00001000的
而且引用是不开辟内存的,而指针是需要开辟内存的
不过引用和指针的底层原理是一样的,引用其实是指针的一个包装,但是咱们语法上还是认为引用不开辟内存。
如下是引用与指针的几大不同点,大家从自己的理解角度去记就好,切忌不要去背:
2.内联函数
介绍内敛函数之前,大家需要对函数栈帧有一定的理解,这样才能更好地体会到内敛函数的优势。
推荐这篇文章,还是讲的比较仔细的。
(72条消息) 【C语言】函数——栈帧的创建和销毁_平凡的人1的博客-CSDN博客_c语言销毁栈
大家读完会发现,函数栈帧是很消耗时间的
- 调用函数时,需要跳转到函数的位置,建立函数的栈帧(需要指针去维护栈帧)
- 函数结束需要销毁栈帧
并不是说函数栈帧不好,函数栈帧可以实现高内聚,低耦合,但是一些小函数如何也要去建立函数栈帧的话,就很影响程序的运行时间。
1.c语言为了避免函数栈帧的方式–宏函数
大家可以试一试,写一个Add的int加法宏函数
#define ADD(x,y) ((x)+(y))
前面两个是错误的
-
宏函数是没有类型的,也没有return的 -
宏函数是完完全全的替换,就是有Add(3,5)的地方就完全换成后面的部分,2的这种写法有什么坏处呢 #include<iostream>
using namespace std;
#define ADD(x,y) x+y
int main()
{
cout<<ADD(3,5)*2<<endl;
return 0;
}
按道理来说,上面代码应该会输出16,可事实是输出了13,因为ADD(3,5)*2,他的翻译是 3+5*2 ,而我们的想法是(3+5)*2,所以写宏函数不要吝啬括号。
通过上面几点我们发现宏函数有很多缺点:
- 写法复杂,容易出错(一个Add函数都这么容易出错)
- 无类型判断,不安全
- 无法调试
2.取代宏函数,内敛函数占大部分
c++是建议我们抛弃宏函数的
内联函数的语法:
inline int Add(int x, int y)
{
return x + y;
}
int main()
{
int c = Add(1, 2);
cout << c << endl;
return 0;
}
内敛函数的作用:
我们来看看没加inline 的代码的汇编部分(很多指令):
那一句call Add(0E210FFH) 就是调用函数的指令,接着就会进行跳转到另一个地方,也就是Add函数的部分,建立函数栈帧。
我们来看看加了inline 的代码的汇编部分(很多指令):(当然编译器做了一定的优化)
大家可以看到,调用函数的指令消失了,也就是说就不会建立函数栈帧了。
inline 函数的实质是:去掉 call Add(0E210FFH) 指令,将Add函数里面的指令全部的拷贝到main函数里面,这样就省去了建立和销毁函数栈帧的时间,这是一种用空间换取时间的方式。
内联函数的缺点
不符合程序高内聚,低耦合的理念,每次使用一次函数,main函数里面的指令就一个函数条指令,而建立函数栈帧多的指令只是call Add(0E210FFH) 这一条指令。
举个例子吧:
如果一个函数里面有100条指令,要调用函数10此
那么建立栈帧用函数的话,指令就是110条
而用内敛函数的话,指令就是1000条
所以内敛函数是用空间换取时间的做法
内敛函数的使用范围:
结论:频繁调用的小函数,建立使用inline 函数–因为这是操作系统消耗得起的
如果是一些大函数,消耗得空间很大,不要使用inline 函数,编译器也不会让你去使用的(编译器也是有底线的)
3.自动识别类型auto
#include<iostream>
using namesapce std;
int main()
{
int a = 10;
auto b = a;
cout << a << endl << b << endl;
return 0;
}
b的类型,编译器会根据a的类型来相对应,推出b是int类型
大家可以想一想,下面几种情况auto 分别都识别成了什么类型
#include<iostream>
using namespace std;
int main()
{
int x = 10;
auto a = &x;
auto* b = &x;
int& y = x;
auto c = y;
auto& d = x;
cout << typeid(x).name() << endl;
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(y).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
return 0;
}
大家不要去像int&类型哦,int& b = a的写法是告诉我们,b是a的别名
typeid().name() ,可以告诉我们类型的名字,大家不清楚的时候,用这个来验证以下可以
语法就是在左括号填写变量名即可,右括号是函数调用的意思。
4.范围for
咱们先回顾以下,c语言的范围for的用法
#include<iostream>
using namespace std;
int main()
{
int arr[] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); ++i)
{
cout << arr[i] <<" ";
}
cout << endl;
return 0;
}
c++形式:
#include<iostream>
using namespace std;
int main()
{
int arr[] = { 1, 2, 3, 4, 5 };
for (int a : arr)
{
cout << a << " ";
}
cout << endl;
return 0;
}
for里面放int a: arr 的实质,遍历一遍arr,每一次遍历时,创建一个变量a,将元素值给a,再销毁a
建议这样写for (auto a : arr) ,这样的话,就是是double数组也可以这样写,移植性也就更高。
咱们试着修改以下数组元素的值:
int main()
{
int arr[] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); ++i)
{
arr[i] *= 2;
cout << arr[i] << " ";
}
cout << endl;
for (auto a : arr)
{
a *= 2;
cout << a << " ";
}
cout << endl;
return 0;
}
按道理来说应该会输出
2 4 6 8 10
4 8 12 16 20
可是事实是:
2 4 6 8 10
2 4 6 8 10
也就是说,范围for的这种写法没有改动,数组元素的值。
咱们来思考以下,范围for的原理是:遍历一遍arr,每一次遍历时,创建一个变量a,将元素值给a,再销毁a。
但是这个临时变量a不会改动数组元素的值,所以我们可以建立a与数组元素的联系。
也就是将a设置为数组元素的别名,这样我们就可以修改数组元素了。
方法:将for (auto a : arr) 改成for (auto& a : arr) 即可
5.c++空指针新的表示方法–nullptr
我们先来回顾以下c语言的空指针表示方法NULL,他是这样定义的
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
大家可以理解成NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量
在.c文件中是0,在.cpp 中是void* 0
但是这样会有一个缺陷,咱们用代码来演示以下这个缺陷
#include<iostream>
using namespace std;
void f(int i)
{
cout << "f(int)" << endl;
}
void f(int* p)
{
cout << "f(int*)" << endl;
}
int main()
{
int* p1 = NULL;
int* p2 = nullptr;
f(0);
f(NULL);
f(nullptr);
return 0;
}
按道理来说f(0)是会输出“f(int)”,因为数字0默认是int类型
f(NULL)是会输出"f(int*)",因为NULL是一个指针类型
可是事实是,都是输出"f(int)",这就是c语言下空指针定义法的一个缺陷
而c++下的空指针是这样定义的
注意:
-
在使用nullptr 表示指针空值时,不需要包含头文件,因为nullptr 是C++11作为新关键字引入的。 -
在C++11中,sizeof(nullptr) 与 sizeof((void*)0) 所占的字节数相同。 -
为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr 。
这样就避免的上述代码的问题!
6.类
类的概念理解
都说c语言是面向过程,c++是面向对象,那么具体怎么理解呢?
大家看看这段栈的代码
#include<iostream>
using namespace std;
typedef int STDatatype;
struct Stack
{
STDatatype* a;
int size;
int capacity;
};
void StackInit(struct Stack* ps);
void StackPush(struct Stack* ps, STDatatype x);
int main()
{
struct Stack st;
StackInit(&st);
StackPush(&st,1);
StackPush(&st,2);
StackPush(&st,3);
StackPush(&st,4);
return 0;
}
c语言更加关注于,接下来我的栈要有 初始化,入栈,出栈,关注于方法,关注于过程 对象与过程时分开的
我们再看看c++下的栈的代码
#include<iostream>
using namespace std;
typedef int STDatatype;
struct Stack
{
void Init(int capacity = 4)
{
a = (STDatatype*)malloc(sizeof(STDatatype)*capacity);
size = 0;
capacity = capacity;
}
void Push(STDatatype x)
{
a[size] = x;
size++;
}
STDatatype* a;
int size;
int capacity;
};
int main()
{
Stack st;
st.Init();
st.Push(1);
st.Push(2);
st.Push(3);
st.Push(4);
return 0;
}
对象不仅仅有成员对象,还有成员函数 并不是说c++没有过程,而是说更加关注于对象,对象与方法时一体的
而且我们可以看到,写法上c++也在强调Init Push 的接口是对象st 的(c语言习惯叫变量,c++更偏向于叫对象)
我们看到上述类的代码中,函数的定义放在了域里面,能不能只将声明放在域里面,定义放在其他地方呢,答案是可以的。
typedef int STDatatype;
struct Stack
{
void Init(int capacity = 4);
void Push(STDatatype x);
STDatatype* a;
int size;
int capacity;
};
void Stack::Init(int capacity = 4)
{
a = (STDatatype*)malloc(sizeof(STDatatype)*capacity);
size = 0;
capacity = capacity;
}
void Stack::Push(STDatatype x)
{
a[size] = x;
size++;
}
void Stack::Init(int capacity = 4) ,这种写法是因为,类的生命周期是域,出了域就不可以用了,所以咱们要指明是Stack 里面的Init 成员函数
类的二种声明方式–struct 和class
没错,出了用struct 来声明类,class 也可以
大家可以将上面的代码的struct 换成class 来试一试,但是居然编译不过去,等等,不是骗你们的。
这是因为struct 和class 定义的类,访问权限是不一样的。
那咱们聊一聊访问权限是啥?
就像你写了一个项目仓库一样,项目仓库的访问权限可以是1. 仅自己可见 2.所有人可见。
struct 和class 也是一样的,struct 创建的类,他的默认权限是公开的(public ),不仅域里面的可以调用,域之外的都可以调用,所以不会报错。
而class 则相反,默认权限是私有的(private ),除了自己域里面可以用,域之外的是不可以用的。
那我们如何去控制权限呢,一般我们都会把成员函数给设置为公用权限,而成员对象设置为私有权限,我们如果想改变成员对象也只能通过调用成员函数。
class Stack
{
public:
void Init(int capacity = 4);
void Push(STDatatype x);
private:
STDatatype* a;
int size;
int capacity;
};
只要我们这样设置了权限之后,编译器就不会报错了。
类的空间大小
类的对象是如何存储的
c++的类的对象与c语言的变量的成员函数是类似的,他们都遵循一个内存对齐的原则,所以咱们先回顾以下内存对齐。
内存对齐的规则如下:
1.结构体的内存对齐规则
-
第一个成员在结构体变量偏移量为0的地址处 -
其他成员变量要对齐到(对齐数)的整数倍的地址处 对齐数 = 编译器默认的一个对齐数 与 该成员大小的 较小值(二者的一个较小值) vs中默认的对齐数为8 -
结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍 -
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
2.内存对齐的目的
主要原因可以归结为两点:
-
有些CPU可以访问任意地址上的任意数据,而有些CPU只能在特定地址访问数据,因此不同硬件平台具有差异性,这样的代码就不具有移植性,如果在编译时,将分配的内存进行对齐,这就具有平台可以移植性了 -
CPU每次寻址都是要消费时间的,并且CPU 访问内存时,并不是逐个字节访问,而是以字长(word size)为单位访问,所以数据结构应该尽可能地在自然边界上对齐,如果访问未对齐的内存,处理器需要做两次内存访问,而对齐的内存访问仅需要一次访问,内存对齐后可以提升性能。举个例子:
假设当前CPU是32位的,并且没有内存对齐机制,数据可以任意存放,现在有一个inta变量占4byte,存放地址在0x00000002 - 0x00000005(纯假设地址,莫当真),这种情况下,每次取4字节的CPU第一次取到[0x00000000 - 0x00000003],只得到变量1/2的数据,所以还需要取第二次,为了得到一个int32类型的变量,需要访问两次内存并做拼接处理,影响性能。如果有内存对齐了,int32类型数据就会按照对齐规则在内存中,上面这个例子就会存在地址0x00000000处开始,那么处理器在取数据时一次性就能将数据读出来了,而且不需要做额外的操作,使用空间换时间,提高了效率。 ————————————————
内存对齐目的来源于文章:
原文链接:https://blog.csdn.net/qq_39397165/article/details/119745975
类的成员函数是如何存储的,难道适合对象一起放在那个域里面吗,答案是否定的,如果真是那样的话,就会造成大量的空间浪费。
本质上类的成员对象是不同的,但是每次调用类的成员函数都是相同的那个成员函数。
所以,成员函数并不是存放在对象里面的而是和对象分开放的。
图解:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m7kvMGXk-1662084368991)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20220902094728234.png)]
总结:类的空间大小就是类的成员变量所占的空间大小(但是遵循内存对齐),不算入成员函数的空间大小,成员函数是不存放在对象中的
#include<iostream>
using namespace std;
class A1 {
public:
void f1(){}
private:
int _a;
};
class A2 {
public:
void f2() {}
};
class A3
{};
int main()
{
A1 aa;
cout << sizeof(aa) << endl;
A2 a2;
A2 aa2;
A2 aaa2;
cout << sizeof(a2) << endl;
cout << &a2 << endl;
cout << &aa2 << endl;
cout << &aaa2 << endl;
A3 a3;
cout << sizeof(a3) << endl;
return 0;
}
总结:当类没有成员变量时,会用一个字节在内存中占一个位置,说明对象存在过。
下课!
|