IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> c++入门语法收尾,类与对象(上) -> 正文阅读

[C++知识库]c++入门语法收尾,类与对象(上)

c++入门语法收尾,类与对象(上)

1.引用与指针大不同点

代码演示:

int main()
{
	int a = 10;

    //在语法上,这里给a这块空间取了一个别名,没有新开空间
	int& ra = a;
	ra = 20;

	cout << ra << endl;

	return 0;
}

#include<iostream>
using namespace std;

int main()
{
    //在语法上,这里定义个pa指针变量,开了4个字节,存储a的地址
    int a = 10;
    int* pa = &a;
	*pa = 20;
    return 0;
}

图片演示:

a都是一块空间的名字,ra是a的别名,所以也是这块空间的名字

这块空间的地址是0x00001000,指针pa是用来装0x00001000的

而且引用是不开辟内存的,而指针是需要开辟内存的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BtLvwaFb-1662084368985)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20220901160938255.png)]


不过引用和指针的底层原理是一样的,引用其实是指针的一个包装,但是咱们语法上还是认为引用不开辟内存。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n7DM5zRB-1662084368986)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20220901161309195.png)]


如下是引用与指针的几大不同点,大家从自己的理解角度去记就好,切忌不要去背:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fZCCkoCN-1662084368987)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20220901161432835.png)]


2.内联函数

介绍内敛函数之前,大家需要对函数栈帧有一定的理解,这样才能更好地体会到内敛函数的优势。

推荐这篇文章,还是讲的比较仔细的。

(72条消息) 【C语言】函数——栈帧的创建和销毁_平凡的人1的博客-CSDN博客_c语言销毁栈

大家读完会发现,函数栈帧是很消耗时间的

  1. 调用函数时,需要跳转到函数的位置,建立函数的栈帧(需要指针去维护栈帧)
  2. 函数结束需要销毁栈帧

并不是说函数栈帧不好,函数栈帧可以实现高内聚,低耦合,但是一些小函数如何也要去建立函数栈帧的话,就很影响程序的运行时间。


1.c语言为了避免函数栈帧的方式–宏函数

大家可以试一试,写一个Add的int加法宏函数

//#define ADD(int x,int y) return x+y;   //error
//#define ADD(x,y) x+y;   //error
//#define ADD(x,y)  (x+y);//error--宏函数是不能加分号的
#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,所以写宏函数不要吝啬括号。


通过上面几点我们发现宏函数有很多缺点:

  1. 写法复杂,容易出错(一个Add函数都这么容易出错)
  2. 无类型判断,不安全
  3. 无法调试

2.取代宏函数,内敛函数占大部分

c++是建议我们抛弃宏函数的

内联函数的语法:
inline int Add(int x, int y)//在返回类型的前面加上inline即可
{
	return x + y;
}

int main()
{
	int c = Add(1, 2);
	cout << c << endl;

	return 0;
}

内敛函数的作用:

我们来看看没加inline的代码的汇编部分(很多指令):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ATw9vAPF-1662084368987)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20220901164216460.png)]

那一句call Add(0E210FFH)就是调用函数的指令,接着就会进行跳转到另一个地方,也就是Add函数的部分,建立函数栈帧。


我们来看看加了inline的代码的汇编部分(很多指令):(当然编译器做了一定的优化)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RbXRjzAC-1662084368988)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20220901164507788.png)]

大家可以看到,调用函数的指令消失了,也就是说就不会建立函数栈帧了。

inline函数的实质是:去掉 call Add(0E210FFH)指令,将Add函数里面的指令全部的拷贝到main函数里面,这样就省去了建立和销毁函数栈帧的时间,这是一种用空间换取时间的方式


内联函数的缺点

不符合程序高内聚,低耦合的理念,每次使用一次函数,main函数里面的指令就一个函数条指令,而建立函数栈帧多的指令只是call Add(0E210FFH)这一条指令。

举个例子吧:

如果一个函数里面有100条指令,要调用函数10此

那么建立栈帧用函数的话,指令就是110条

而用内敛函数的话,指令就是1000条

所以内敛函数是用空间换取时间的做法


内敛函数的使用范围:

结论:频繁调用的小函数,建立使用inline函数–因为这是操作系统消耗得起的

如果是一些大函数,消耗得空间很大,不要使用inline函数,编译器也不会让你去使用的(编译器也是有底线的)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n7f380A4-1662084368989)(D:\gitee仓库\博客使用的表情包\底线f.gif)]


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;  // int* 
	auto* b = &x; //auto*是明确b是一个指针类型, int*
	int& y = x;   // y的类型是什么?int
	auto c = y;  // int 
	auto& d = x; // d的类型是int, 但是这里指定了d是x的引用

	// 打印变量的类型

	cout << typeid(x).name() << endl;//type identify
	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()
{
	//读一遍数组,c语言方式
	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 };
	

	//c++的方式:范围for
	for (int a : arr)//意思就是遍历一遍arr,每一次遍历时,将元素值给a
	{
		cout << a << "  ";//输出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* p1 = 0; 
	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++下的空指针是这样定义的

注意:

  1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。

  2. 在C++11中,sizeof(nullptr)sizeof((void*)0)所占的字节数相同。

  3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用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;
	}
	//注意,这里用a,size,capacity时没有再像以前一样用 ps->a了,对象和方法时一体的
	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++更偏向于叫对象)


我们看到上述类的代码中,函数的定义放在了域里面,能不能只将声明放在域里面,定义放在其他地方呢,答案是可以的。


//xxx.h
typedef int STDatatype;

struct Stack
{

	void Init(int capacity = 4);

	void Push(STDatatype x);
	
	STDatatype* a;
	int size;
	int capacity;
};

//xxx.cpp
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成员函数


类的二种声明方式–structclass

没错,出了用struct来声明类,class也可以

大家可以将上面的代码的struct换成class来试一试,但是居然编译不过去,等等,不是骗你们的。

这是因为structclass定义的类,访问权限是不一样的。


那咱们聊一聊访问权限是啥?

就像你写了一个项目仓库一样,项目仓库的访问权限可以是1. 仅自己可见 2.所有人可见。

structclass也是一样的,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.结构体的内存对齐规则
  1. 第一个成员在结构体变量偏移量为0的地址处

  2. 其他成员变量要对齐到(对齐数)的整数倍的地址处

    对齐数 = 编译器默认的一个对齐数 与 该成员大小的 较小值(二者的一个较小值)

    vs中默认的对齐数为8

  3. 结构体总大小最大对齐数(每个成员变量都有一个对齐数)的整数倍

  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BtBfJUt0-1662084368990)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20220902093824794.png)]

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;

	// 他们的大小是1,为什么呢? 大小是1,给1个byte不是为了存储数据,是占位,表示对象存在过
	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;
}

总结:当类没有成员变量时,会用一个字节在内存中占一个位置,说明对象存在过。


下课!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-78uaIKYr-1662084368991)(D:\gitee仓库\博客使用的表情包\给点赞吧.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-plRIuInC-1662084368992)(D:\gitee仓库\博客使用的表情包\要赞.jpg)]

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-09-04 00:51:42  更:2022-09-04 00:52:03 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/13 9:49:28-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码