一、auto关键字
C++中可以使用typeid打印变量的类型
#include<iostream>
using namespace std;
int main()
{
int a = 0;
int b = a;
auto c = a;
auto d = 1 + 1.11;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
return 0;
}
但是上面看不出来auto的实际作用,但是auto对于类型很长的名字的作用非常大!
如果一个函数需要运行很多次,普通的函数需要创建多次函数栈帧,造成效率低下!此时我们可以使用宏函数!
宏函数:通常在预处理阶段进行展开,使用宏定义(如C语言中的#define
)来替代代码。它们不占用内存空间,直接在编译时替换。
- 优点:不需要建立栈帧,提高调用效率;
- 缺点:复杂,容易出错,可读性差,不能调试!
Auto的补充:
1、Auto的使用规则
int main()
{
int x = 10;
auto a = &x;
auto* b = &x; // b和a是等价的!
auto& c = x; // c是x的引用!
return 0;
}
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须
加&!
auto常用于for循环:
int main()
{
int x = 10;
auto a = &x;
auto* b = &x; // b和a是等价的!
auto& c = x; // c是x的引用!
int arr[] = { 1,2,3,4,5 };
for (auto& x : arr)
{
x++;
}
for (auto& x : arr)
{
cout <<x<<endl;
}
return 0;
}
for (auto& x : arr):这是一个范围基于的 for 循环,auto& 表示 x 是 arr 中每个元素的引用。使用引用可以避免复制元素,提高效率。
2、Auto不能推导的场景
- auto不能作为函数的参数
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}
- auto不能直接用来声明函数
void TestAuto()
{
int a[] = {1,2,3};
auto b[] = {4,5,6};
}
3. 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
4. auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有
lambda表达式等进行配合使用。
二、内联函数
引入:内联函数
概念:以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
缺点:
每次调用的指令都是一样的,那么当调用多次,重复的指令不会相乘,而是相加起来!
内联展开是将Func中的50条指令插入到调用的位置上去!(导致可执行程序变大!)
因此,内联函数适用于短小的频繁调用的函数!
需要注意的是:inline对于编译器仅仅只是一个建议,最终是否成为inline,需要编译器自己决定!
像下面类似的函数就算加上了inline也会被否决掉!
- 比较长的函数;
- 递归函数;
默认的debug模式下,inline不会起作用,否则不方便调试!(默认如果起作用,相当于指令展开了,没有创建函数栈帧,无法进入函数内部,不能调试!)
改成releas可以起作用,但是releas不容易查看汇编代码!(需要自己设置)
- 右键->属性->C/C++ -> 常规 右边的调试信息格式改为程序数据库;
- C/C++ -> 优化 右边的内联函数扩展 ,改为只适用!
此时再次运行就会发现,指令中没有call指令!
但是如果我们对函数做一些修改,将函数体变长!
此时就算我们设置了,编译器仍有可能不会执行inline!
注意点:
inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址
了,链接就会找不到!
此时会显示:编译通过了,但是无法链接上!
从声明处得知:该函数是一个内联函数,此时编译器会想把内联函数展开,但是此时只有声明没有地址!因此编译器会制动call地址从符号表,但是找不到,因此发生链接错误!(内联函数不会进入符号表,不会生成地址!)
因此我们采用下面方式:
直接将函数的声明+实现写在.h文件中!(可以只写定义,此时用到的地方直接拿来展开!)
三、指针空值nullptr
在传统的C头文件(stddef.h)中,可以看到如下代码:
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);
f(NULL);
f((int*)NULL);
return 0;
}
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);
f(NULL);
f((int*)NULL);
f(nullptr);
return 0;
}
nullptr会自动匹配指针类型!
注意点:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引的。
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
为什么函数的参数类型为int*,但是可以传入void*的空指针呢?
在 C 和 C++ 中,当函数的参数类型为 `int*`,而传入的指针类型为 `void*` 时,传入空指针(如 `NULL` 或 `nullptr`)时会发生隐式转换,但需要注意以下几点:
1. 空指针的隐式转换:如果你传入的是空指针(例如 `NULL` 或 `nullptr`),那么无论是 `void*` 还是其他类型的指针,都会被视为有效的空指针。空指针可以被隐式转换为任何类型的指针,包括 `int*`。例如:
void* ptr = nullptr; // 或者 ptr = NULL;
int* intPtr = ptr; // 隐式转换,合法
2. 非空指针的转换:如果你尝试将一个非空的 `void*` 指针传递给一个期望 `int*` 的函数,编译器不会自动进行转换。你需要显式地将 `void*` 转换为 `int*`,例如:
void* ptr = /* some valid memory */;
int* intPtr = static_cast<int*>(ptr); // 显式转换
3. 函数参数:如果函数的参数是 `int*`,并且你传入的是 `void*`,编译器会要求你进行显式转换,除非你传入的是空指针(如 `nullptr`)。例如:
void func(int* p) {
// 函数体
}
void* voidPtr = nullptr; // 空指针
func(static_cast<int*>(voidPtr)); // 显式转换
4. **C++11 及以后的版本**:在 C++11 及以后的版本中,使用 `nullptr` 是推荐的做法,它是一个类型安全的空指针常量,避免了 `NULL` 可能引起的混淆。
总结:当函数的参数为 `int*`,而传入的指针类型为 `void*` 时,传入空指针会自动被视为有效的空指针,但对于非空指针,必须进行显式转换。空指针的隐式转换是安全的。
类和对象
一、初步认识
二、引入
struct升级成了类
struct stack
{
int* a;
int top;
int capicity;
};
int main()
{
struct stack s1;
stack s2;
return 0;
}
类名就是类型!
因此我们可以采用以前C语言的定义方式来定义s1;
还可以使用C++的方式来定义s2;
类里面可以定义重名的函数(类里面也定义了一个域---类域)
C++/C中只要是花括号括起来的,就是定义了一个域!
成员函数和成员变量的顺序没有要求!
示例:
typedef int DataType;
struct Stack
{
// 定义成员变量
DataType* arr;
size_t size;
size_t capacity;
// 定义成员函数
void Init(size_t capacity = 8)
{
arr = (DataType*)malloc(sizeof(DataType) * capacity);
if (arr == nullptr)
{
perror("malloc fail");
return;
}
size = 0;
capacity = capacity;
}
void Push(const DataType& data)
{
// 扩容
arr[size] = data;
++size;
}
DataType Top()
{
return arr[size - 1];
}
void Destory()
{
if (arr)
{
free(arr);
arr = nullptr;
capacity = 0;
size = 0;
}
}
};
int main()
{
struct Stack s;
s.Init(10);
s.Push(1);
s.Push(2);
s.Push(3);
cout << s.Top() << endl;
s.Destory();
Stack s2;
s2.Init();
s2.Push(2);
s2.Push(3);
s2.Destory();
return 0;
}
三、类的定义
C++中将struct升级为了一种特殊的类(主要是为了兼容以前的C):
在 C++ 中,`class` 和 `struct` 都用于定义用户自定义的数据类型,但它们之间有一些关键的区别:
1. 默认访问控制:
- class:默认的成员访问控制是 `private`。这意味着如果没有显式指定访问修饰符,类的成员(变量和函数)将是私有的。
- struct:默认的成员访问控制是 `public`。这意味着如果没有显式指定访问修饰符,结构体的成员将是公共的。
class MyClass {
int x; // 默认 private
};
struct MyStruct {
int x; // 默认 public
};
2. 继承的默认访问控制:
- class:在继承时,默认的继承访问控制是 `private`。这意味着如果没有显式指定继承类型,基类的成员将被视为私有。
- struct:在继承时,默认的继承访问控制是 `public`。这意味着如果没有显式指定继承类型,基类的成员将被视为公共。
class Base {};
class Derived : Base {}; // 默认 private 继承
struct BaseStruct {};
struct DerivedStruct : BaseStruct {}; // 默认 public 继承
3. 用途:
- class:通常用于定义具有封装、继承和多态特性的复杂数据类型,强调数据的隐藏和保护。
- struct:通常用于定义简单的数据结构,强调数据的公共访问,常用于数据聚合。
4. 功能:
- 在 C++ 中,`class` 和 `struct` 在功能上是相同的。两者都可以包含成员变量、成员函数、构造函数、析构函数、运算符重载等。唯一的区别在于默认的访问控制。
5. 兼容性:
- `struct` 可以被视为一种特殊类型的 `class`,因此可以在 `struct` 中使用所有 `class` 的特性。
总结:`class` 和 `struct` 的主要区别在于默认的访问控制和继承方式。选择使用 `class` 还是 `struct` 通常取决于设计意图和代码的可读性。
四、访问限定符
C++一共有三种访问限定符
注意点:
1. public修饰的成员在类外可以直接被访问
2. protected和private修饰的成员在类外不能直接被访问,但是在类中可以调用;(此处protected和private是类似的)(暂定认为protected和private是一样的!)
3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
4. 如果后面没有访问限定符,作用域就到 } 即类结束。
5. class的默认访问权限为private,struct为public(因为struct要兼容C)
问题:C++中struct和class的区别是什么?
解答:C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类默认访问权限是private。注意:在继承和模板参数列表位置,struct和class也有区别,后序给大家介绍。
五、类的定义方式
类的定义方式有两种:
- 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内
联函数处理。因此C++一般长的函数不会在类中定义,但是如果真的在类中定义了代码较长的函数,编译器会自动处理为不是内联!
- 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::
命名规则的规范化:
// 我们看看这个函数,是不是很僵硬?
class Date
{
public:
void Init(int year)
{
// 这里的year到底是成员变量,还是函数形参?
year = year;
}
private:
int year;
};
在 Init 函数中,参数 year 和类的成员变量 year 同名。这会导致一个问题:在函数体内,year 默认指向的是函数的参数,而不是类的成员变量。 (局部域优先)
代码中的 year = year; 实际上是将函数参数 year 赋值给自己,这样的赋值没有任何效果,因为它只是将参数的值赋给了参数本身。类的成员变量并没有被初始化或赋值。
所以一般采用下面的方法:
class Date
{
public:
void Init(int year)
{
_year = year;
}
private:
int _year;
};
// 或者这样
class Date
{
public:
void Init(int year)
{
mYear = year;
}
private:
int mYear;
};
使用_/m修饰成名变量,将成员变量和参数进行区分!
六、封装
封装本质上是一种管理,让用户更方便使用类。比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件。
对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的,CPU内部是如何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以
及键盘插孔等,让用户可以与计算机进行交互即可。
在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
上面的代码就采用了封装的思想(将具体操作封装成一个函数),下面的代码就是由于没有封装,因此看着比较乱;
采用这种方式将数据和方法封装成一个类,将方法设置为公有,数据设置为私有;
此时使用者只能通过函数来访问,而不能直接访问数据!
七、类的作用域
目前遇到了类域,局部作用域,全局域,命名空间域!
- 在不同的域中可以定义同一变量!
- 域会限制对它的访问,编译器有自己的搜索规则,一般现在局部域进行访问,如果在类中会先在类域中进行搜索!类中的搜索顺序是局部-> 类中->全局(命名空间)
- 局部域和全局域会影响生命周期;(局部变量和全局变量的作用域(即它们可以被访问的范围)直接影响它们的生命周期。局部变量的生命周期较短,仅在其所在的函数或代码块内有效,而全局变量的生命周期较长,贯穿整个程序的执行。)
- 类域和命名空间域不会影响生命周期;(类域和命名空间域的作用域并不改变变量的生命周期。类的成员变量的生命周期依赖于对象的生命周期,而命名空间中的变量的生命周期与全局变量相同。因此,虽然它们的作用域可能会影响变量的可见性,但不会影响它们的创建和销毁时机。)
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 ::
作用域操作符指明成员属于哪个类域。
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
cout << _name << " "<< _gender << " " << _age << endl;
}
在.h中的成员变量是声明而不是定义!
对于变量来说,声明和定义真正的区别是开不开空间!(不开空间就是声明!)
在.cpp文件中进行这样的操作就是定义了一个对象!(类实例化对象,也叫做对象定义!)
直接通过类访问是声明,不能进行初始化!
但是可以通过实例化对象来进行初始化!
八、实例化对象的大小
结论:对象的大小只考虑成员变量,不考虑成员函数!
- 210和213两行调用的是不同的成员变量;
- 211和214调用的是同一个成员函数!