类和对象(上)

目录

[一 . inline内联函数](#一 . inline内联函数)

[二 . nullptr](#二 . nullptr)

[三 . 类的定义](#三 . 类的定义)

[3.1 类定义格式](#3.1 类定义格式)

[3.2 访问限定符](#3.2 访问限定符)

[3.3 类域](#3.3 类域)

[四 . 实例化](#四 . 实例化)

[4.1 实例化概念](#4.1 实例化概念)

[4.2 对象大小](#4.2 对象大小)

[五 . this 指针](#五 . this 指针)

[六 . C++和C语言实现Stack 对比](#六 . C++和C语言实现Stack 对比)


一 . inline内联函数

1 . 用 inline 修饰的函数叫做内联函数 , 编译时C++编译器会在调用的地方展开内联函数 , 这样调用内联函数就 不需要建立栈帧了 , 就可以提高效率

2 . inline 对于编译器而言就是一个建议 , 也就是说 , 加了 inline 编译器也可以选择在调用的地方不展开 , 不同编译器关于 inline 什么情况展开各不相同 , 因为 C++ 标准没有规定这个 。inline 适用于 频繁调用的短小函数对于递归函数 , 代码相对较多的函数 , 加上 inline 也会被编译器忽略 。

3 . C语言实现宏函数也会在预处理时替换展开 , 但是宏函数实现很复杂很容易出错 , 且不方便调试 ,C++设计了inline 目的就是替换 C 的宏函数 。

1 . 宏 ---> 不加分号 --> 因为宏相当于一个替换机制 (预处理阶段进行替换)

---> 以下代码的 1+2 被替换成了 1+2;

2 . 宏-->外层需要括号

3. 宏-->内层需要括号

int ret = Add(1,2) ----> int ret = ( ( 1 ) + ( 2 ) ) ;

4 . VS编译器debug 版本下面默认是不展开 inline 的, 这样方便调试 , debug 版本想展开需要设置 一下两个地方。

inline 的 最终解释权还是归 编译器所有 ;

5 . inline 不建议声明和定义分离到两个文件 , 分离会导致链接错误 。 因为inline 被展开 , 就没有函数地址 , 链接时就会出现报错 。

//F.cpp
#include "F.h"

void f(int i)
{
	cout << i << endl;
}


//F.h
#pragma once
#include <iostream>
using namespace std;

inline void f(int i);

//test.cpp
#include "F.h"
int main()
{
//链接错误:LNK2019 无法解析的外部符号 "void __cdecl f(int)" (? f@@YAXH@Z),函数 main 中引用了该符号

	f(10);
	return 0;
}

//正确用法 -- F.h
inline void f(int i)
{
	cout << i << endl;
}

思考 : 普通函数为什么不能放在 .h 头文件里 ?

这里需要了解以下源文件 编译链接的过程 : 编译和链接-CSDN博客

二 . nullptr

NULL 实际是一个宏 , 在传统的C头文件 ( stddef.h) 中 可以看到如下的代码 :

ifndef NULL

ifdef __cplusplus

define NULL 0

else

define NULL ((void *)0)

endif

endif

C++ 中NULL可能被定义为 字面常量0 ,或者C中被定义为无类型指针(void*) 的常量 。 不论采取何种定义 , 在使用空值的指针时 , 都不可避免会遇到一些麻烦 , 本想通过 f(NULL) 调用指针版本的f(int*) 函数 , 但是由于NULL被定义成了0 , 调用了 f(int) , 因此与程序的初衷相违背 。 f((void*) NULL) ; 调用会报错 ;

C++11 中引入了nullptr , nullptr 是一个特殊的关键字 , nullptr 是一种特殊类型的字面量 , 它可以转换成任意其他类型的 指针类型 。 使用 nullptr 定义空指针可以避免类型转换的问题 , 因为nullptr 只能被隐式地转换为指针类型 , 而不能被转换为整数类型 。

#include <iostream>
using namespace std;

void f(int x)
{
	cout << "f(int x)" << endl;
}
void f(int* ptr)
{
	cout << "f(int* ptr)" << endl;
}
int main()
{
	f(0);
	f(NULL);
	f((int*)NULL);
	f(nullptr);
	return 0;
}

三 . 类的定义

3.1 类定义格式

  1. class 为定义类的关键字, { } 中为类的主体 , 注意类定义结束时 后面分号不能省略 。

类体中内容称为类的成员 : 类中的变量称为类的属性或成员变量 ; 类中的函数称为类的方法或者成员函数 。

2 . 为了区分成员变量 , 一般习惯上成员变量会加上 一个特殊标识 , 如成员变量前面或者后面加 _ 或者 m 开头 , 注意C++中这个并不是强制的 , 只是一个惯例 , 具体的看公司的要求 。

3 . C++中struct 也可以定义类 , c++ 兼容C中struct 的用法 , 同时 struct 升级成了 类 , 明显的变化是 struct 可以定义函数 , 一般情况下我们还是推荐用class定义类 。

4 . 定义在类 的成员函数默认为 inline 。

下面对以上概念做举例说明 :

1 . 一般成员变量建议加一些特殊标志,避免如下情况:

加上标志后:

2 . C++的 struct 升级了类 ---> 类里面可以定义函数 ; struct 名称可以代表类型

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

//C++升级struct 升级成了类
//1.类里面可以定义函数
//2.struct 名称就可以代表类型

//C++兼容C中struct 的用法
typedef struct ListNodeC
{
	struct ListNodeC* next;
	int val;
}LTNode;

//不再需要typedef,ListNodeCpp就可以代表类型
struct ListNodeCpp
{
	void Init(int x) {
		next = nullptr;
		val = x;
	}
	ListNodeCpp* next;
	int val;
};

int main()
{
	LTNode node;
	struct ListNodeC* node2;
	ListNodeCpp node3;
	return 0;
}

3.2 访问限定符

1 . C++一种实现封装 的方式 , 用类将对象的属性与方法结合在一块 , 让对象更加完善 , 通过访问权限选择性 , 将其接口提供给外部的用户使用 。

2 .public 修饰的成员在类外可以直接被访问 ; protected 和 private 修饰的成员在类外不能直接被访问, protected 和 private 是一样的 , 具体的区别需要在继承章节 , 才能体现出来 。

3 . 访问限定作用域从该访问限定符出现位置开始 直到下一个访问限定符出现时为止, 如果后面没有访问限定符 , 作用域直到 } 即类结束 。

4**. class 定义成员没有被访问限定符修饰时 , 默认为private , struct 默认为public 。**

5 . 一般成员变量都会被限制为private/protected , 需要给被人使用的成员函数会放为public 。

1 . class 定义成员没有被访问限定符修饰时 , 默认为private , struct 默认为public 。

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

class Date
{
	//class默认为private
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
	d1.Init(2024,11,13);
	return 0;
}

访问限定符 限制 的是 类外 的使用 。

3.3 类域

1 . 类定义了一个新的作用域,类的所有成员都在类的作用域中 ,在类体外定义成员时,需要使用 :: 作用域操作符 指明成员属于那个类域 。

2 . 类域影响的是编译的查找规则, 下面程序中 Init 如果不指定类域Stack , 那么编译器就会把Init 当成全局函数 , 那么编译时 , 找不到 array 等成员的声明 / 定义在哪里 , 就会报错 。 指定类域 Stack , 就是直到 Init 是成员函数 , 当前域找不到 array 等成员 , 就会到类域去查找 。

不同的类可以定义同一个函数/变量 , 并不会冲突 , 因为作用域不同 , 类定义了一个新的作用域 --- 类域 ,类 与 类之间的作用域隔离 。

类的声明 : 需要注意的是 --> 成员函数只声明 , 不定义 , 并且如果需要加缺省参数 , 只能在声明中添加 。

类的定义 : 使用 某类的成员时 , 需要指明类域 。不指明类域时 , 编译查找规则是( 局部域 --> 全局域) , 当指明作用域时 , 编译查找规则是 ( 局部域 --> 类域 --> 全局域 )

//Stack.h
#pragma once
#include<iostream>
using namespace std;
class Stack
{
public:
	// 成员函数 -- 只声明不定义
	void Init(int n = 4);

private:
	// 成员变量
	int* array;
	size_t capacity;
	size_t top;
};


//Stack.cpp
#include "Stack.h"

// 声明和定义分离,需要指定类域
void Stack::Init(int n)
{
	array = (int*)malloc(sizeof(int) * n);
	if (nullptr == array)
	{
		perror("malloc申请空间失败");
		return;
	}
	capacity = n;
	top = 0;
}

//test.c
int main()
{
    Stack st;
    st.Init();
    return 0;
}

四 . 实例化

4.1 实例化概念

1 . 用类 类型 在物理内存中创建对象的过程 , 称为类实例化出对象 。

2 . 类是对象进行一种抽象描述 , 是一个模型一样的东西 , 限定了类有哪些成员变量 , 这些成员变量只是声明 , 没有分配空间 , 用类实例化出对象时 , 才会分配空间 。

3 . 一个类可以实例化出多个对象 , 实例化出的对象 占用实际的物理空间 , 存储类成员变量 ,

打个比方 : 类实例化出对象就像现实中使用建筑设计图建造出房子 , 类就像是设计图 , 设计图规划了有多少个房间 , 房间大小功能等 , 但是并没有实体的建筑存在 , 也不能住人 , 用设计图修建出放在 , 放在才能住人 。 同样类就像设计图一样 , 不能存储数据 , 实例化出的对象分配物理内存存储数据 。

!!! 对于变量声明和定义区别 : 是否开辟空间

//test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include "Stack.h"

int main()
{
	//类实例化对象 --> 开空间
    //对象的定义,也是成员变量的定义
    //因为成员变量也是对象的一部分 
	Stack st1;
	Stack st2;
	Stack st3;
	return 0;
}

4.2 对象大小

只计算成员变量 , 不计算成员函数 , 遵循内存对齐 ,如果是空类 --- 1 byte ;

#define _CRT_SECURE_NO_WARNINGS 1
#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(2024, 11, 13);
	d1.Print();

	Date d2;
	d2.Init(2024, 11, 14);
	d2.Print();
	return 0;
}

C++规定类实例化的对象也要符合内存对齐的规则 :

  1. 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处

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

对齐数 = 编译器默认的一个对齐数 与 该成员变量大小的较小值

-vs 中默认为8

-Linux中gcc 没有默认对齐数,对齐数就是成员自身的大小

  1. 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍。

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

想要再详细了解对齐规则 :自定义类型:结构体-CSDN博客

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;


// 计算⼀下A/B/C实例化的对象是多⼤?
class A
{
public:
	void Print()
	{
		cout << _ch << endl;
	}
private:
	char _ch;
	int _i;
};
class B
{
public:
	void Print()
	{
		//...
	}
};
class C
{};
int main()
{
	A a;
	B b;
	C c;
	cout << sizeof(a) << endl;
	cout << sizeof(b) << endl;
	cout << sizeof(c) << endl;
	return 0;
}

上面的程序运行后 , 成员变量B和C类的对象大小是1 , 为啥呢 ?

----> 因为如果一个字节都不给 , 怎么证明对象存在过呢 ! 所以这里给 1 字节 ,

存粹是为了占位-->标识对象的存在。

五 . this 指针

1 . Date 类中有Init 与 Print 两个成员函数 , 函数体中没有关于不同对象的区分 , 当 d1 调用 Init 和 Print 函数是 , 该函数通过一个隐式的this 指针解决这里的问题

2 . 编译器编译后 , 类的成员函数默认都会在形参第一个位置 , 添加一个当前类型的指针,叫做 this 指针 .

3 . 类的成员函数中访问成员变量 , 本质上 都是通过this 指针访问的

4 . C++ 规定不能再实参和形参的位置先使的写this 指针( 编译时编译器会处理) , 但是可以再函数体内显示使用this指针 。

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

class Date
{
public:
	//void Init(Date* const this, int year, int month, int day)
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//void Print(Date* const this)
	void Print()
	{
		//cout <<this-> _year << "/" << this->_month << "/" <<this-> _day << endl;
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	//只是声明,没有定义(开空间)
	 this->_year = year;
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
	//d1.Init(&d1,2024, 11, 13);
	d1.Init(2024, 11, 13);
	//	d1.Print(&d1);
	d1.Print();

	Date d2;
	//	d2.Init(&d2,2024, 11, 14);
	d2.Init(2024, 11, 14);
	// d2.Print(&d2);
	d2.Print();
	return 0;
}

1 . 先排除E --> 因为this 指针不占对象的空间

  1. 再排除B --> 因为动态内存开辟是在堆上

3 . 再排除C --> 因为全局变量,静态变量的生命周期是全局

  1. 下面打印整型常量的地址和常变量的地址对D做解释,又因为this指针的生命周期是与对象有关 ,但常量区的内容不会随着对象而移动 ,故排除D:

5 . this 指针不断变化 ---> 本质上是个形参(参数) , 调用时压栈

另外 :有些编译器会把 this指针存放在寄存器 , VS是通过ecx 传递this指针;存储体系里寄存器时最快的 , 然后是缓存 ,再到内存 。

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;

int main()
{
	const int a = 10;
	int b = 20;
	const char* str = "11111111";
	cout << &a << endl;
	cout << &b << endl;
	cout << (void*)str << endl;
	return 0;
}

这里提一下 : cout 因为可以自动识别数据类型 , 如果想要打印字符串(常变量) 的地址 ,

以下有两种方法 : 1)使用printf ,格式为%p ; 2) 强制类型转换(void*)

六 . C++和C语言实现Stack 对比

面向对象三大特性 : 封装 、继承 、多态 。

**下面的对比我们初步了解一下封装 :**通过下面两份代码对比 , 我们发现C++实现Stack 形态上还是发生了挺多变化 ,但是底层和逻辑上没啥变化 。

1 . C++中数据和函数都放到了类里面 , 通过访问限定符进行了限制 , 不能再随意通过对象直接修改数据 , 这是C++封装的一种体现 , 这个是最重要的变化 。 这里的封装本质是一种更严格规范的管理 , 避免出现乱访问乱修改的问题后续会继续学习封装的思想 。

2 . C++中有一些相对方便的语法 , 比如

1 ) Init 给的缺省参数会方便很多

2 )成员函数每次不需要传对象地址 ,因为 this 指针隐含传递了

3 ) 使用类型不再需要typedef ,可以直接使用类名就很方便

3 . 后续更新的STL中 , 用适配器实现的Stack 会深刻体会到 c++ 的魅力 。

C代码Stack :

#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
typedef int STDataType;
typedef struct Stack
{
    STDataType* a;
    int top;
    int capacity;
}ST;

void STInit(ST* ps)
{
    assert(ps);
    ps->a = NULL;
    ps->top = 0;
    ps->capacity = 0;
}
void STDestroy(ST* ps)
{
    assert(ps);
    free(ps->a);
    ps->a = NULL;
    ps->top = ps->capacity = 0;
}
void STPush(ST* ps, STDataType x)
{
    assert(ps);
    // 满了, 扩容
    if (ps->top == ps->capacity)
        {
        int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
        STDataType * tmp = (STDataType*)realloc(ps->a, newcapacity *sizeof(STDataType));
        if (tmp == NULL)
            {
            perror("realloc fail");
            return;
            }
        
        ps->a = tmp;
        ps->capacity = newcapacity;
        }
    
        ps->a[ps->top] = x;
        ps->top++;
    
}

bool STEmpty(ST * ps)
{
     assert(ps);
   
     return ps->top == 0; 
}

void STPop(ST * ps)
{
    assert(ps);
    assert(!STEmpty(ps));
    ps->top--;
}

STDataType STTop(ST * ps)
{
    assert(ps);
    assert(!STEmpty(ps));
   
    return ps->a[ps->top - 1];
}

int STSize(ST * ps)
{
    assert(ps);
    return ps->top;
}

int main()
{
   ST s;
   STInit(&s);
   
    STPush(&s, 1);
    STPush(&s, 2);
    STPush(&s, 3);
    STPush(&s, 4);
    while (!STEmpty(&s))
        {
        printf("%d\n", STTop(&s));
        STPop(&s);
        }

    STDestroy(&s);
  
    return 0;
}

C++代码Stack :

#include<iostream>
#include<assert.h>
using namespace std;
typedef int STDataType;
class Stack
{
public:
	// 成员函数
	void Init(int n = 4)
	{
		 _a = (STDataType*)malloc(sizeof(STDataType) * n);
		 if (nullptr == _a)
			{
				perror("malloc申请空间失败");
				return;
			}
		
		  _capacity = n;
		  _top = 0;
	}
void Push(STDataType x)
	{
	if (_top == _capacity)
		{
			int newcapacity = _capacity * 2;
			STDataType * tmp = (STDataType*)realloc(_a, newcapacity *sizeof(STDataType));
	if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}

		 _a = tmp;
		_capacity = newcapacity;
		}
		 _a[_top++] = x;
}

void Pop()
	{
	assert(_top > 0);
	 --_top;
	 }

bool Empty()
	 {
	 return _top == 0;
	 }

	 int Top()
	{
	 assert(_top > 0);
	
		 return _a[_top - 1];
	 }

	 void Destroy()
	 {
	 free(_a);
	 _a = nullptr;
	 _top = _capacity = 0;
	 }

		 private:
		// 成员变量
		STDataType * _a;
		size_t _capacity;
		size_t _top;

};
 int main()
{
		Stack s;
		s.Init();
		s.Push(1);
		s.Push(2);
		s.Push(3);
		s.Push(4);

	while (!s.Empty())
	{
		printf("%d\n", s.Top());
		s.Pop();
	}

		s.Destroy();
		return 0;
}
相关推荐
LabVIEW开发5 分钟前
LabVIEW开发相机与显微镜自动对焦功能
算法·计算机视觉·labview知识
禾风wyh6 分钟前
【Pytorch】Python random 模块
开发语言·python
每天写点bug7 分钟前
golang项目三层依赖架构,自底向上;依赖注入trpc\grpc
开发语言·架构·golang
秀儿还能再秀9 分钟前
支持向量机SVM——基于分类问题的监督学习算法
算法·机器学习·支持向量机·学习笔记
我不是程序猿儿11 分钟前
【C++】关于使用系统库fileapi.h的readfile,及’读‘时间耗时太长的解决方案
c++·stm32·单片机
编码追梦人11 分钟前
【C++进阶实战】基于linux的天气预报系统
开发语言·c++
凭君语未可17 分钟前
讲解C语言形参与实参
c语言
single59419 分钟前
【c++笔试强训】(第五篇)
java·开发语言·c++·vscode·学习·算法·牛客
袁代码20 分钟前
SwiftUI开发教程系列 - 第十二章:本地化与多语言支持
开发语言·前端·ios·swiftui·swift·ios开发
A charmer23 分钟前
【C++】list 类深度解析:探索双向链表的奇妙世界
开发语言·c++