目录
[一 . 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 类定义格式
- 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++规定类实例化的对象也要符合内存对齐的规则 :
结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
其他成员变量要对齐到某个数字(对齐数)的整数倍的地址数
对齐数 = 编译器默认的一个对齐数 与 该成员变量大小的较小值
-vs 中默认为8
-Linux中gcc 没有默认对齐数,对齐数就是成员自身的大小
结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍。
如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
想要再详细了解对齐规则 :自定义类型:结构体-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 指针不占对象的空间
- 再排除B --> 因为动态内存开辟是在堆上
3 . 再排除C --> 因为全局变量,静态变量的生命周期是全局
- 下面打印整型常量的地址和常变量的地址对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;
}