C++问题整理【第一章】
文章目录
- C++问题整理【第一章】
- 前言
- 一、C++中指针和引用的区别:
- 二 、从汇编层简述解释一下引用 ?
- 三、 #define 和const 的区别:
- 四、 C++中 typedef 和 define 的区别:
- 五、C++中将引用作为函数的返回值类型的格式~好处和需遵守的规则:
- 六、简述函数的调用过程:
- 七、 C++左值、右值、将亡值的概念:
- 八、 简述 __ stdcall和 __ cdecl的区别
- 九、 C++中const 使用场景
- 十、 简述 strcpy、sprintf 与 memcpy 的区别
- 十一、 简述C++中的指针参数传递和引用参数传递
- 十二、 C++内联函数
- 十三、 简述栈溢出的原因
- 十四、 堆区和栈区
- 十五、 为什么C++可以重载函数?
- 十六、 new, malloc 区别
- 十七、time函数 clock 函数的区别
前言
介绍C++中的引用的相关知识点,typedef及define与const、new与delete、_stdcall与_cdecl等关键字的用法与区别,函数调用过程,C++的值类别(左值、右值、将亡值),字符串函数(strcpy、sprintf和memcpy),指针参数传递与引用参数传递区别,内联函数的用法,栈区与堆区的比较,C++可以重载的原因,time函数与clock函数的区别
一、C++中指针和引用的区别:
(1)语法规则上
-
指针变量存储某个实例(变量或对象)的地址; 引用是某个实例的别名。
-
指针变量:程序为其分配内存区域; 引用:程序不为其分配内存区域。
-
解引用是指针使用时要在前面加" * " ; 引用可以直接使用。
-
指针变量的值可发生改变,存储不同实例地址; 引用在定义时就被初始化,之后不能是被其他实例的引用。
-
指针变量的值可以为空(NULL),但引用没有空引用。
-
理论上指针级别没有限制,引用只有一级。(即不存在引用的引用,但可以有指针的指针)。
-
指针变量作为形参时需要测试它的合法性(判空NULL);引用不需要判空。
-
对指针变量使用“sizeof”得到的是指针变量的大小 ;对引用变量使用“sizeof”得到的是变量的大小。
-
++引用与++指针变量的效果不一样:
对指针变量的操作,会使指针变量指向下一个实体(变量或对象),而不是改变所指实体(变量或对象)的内容。
对引用的操作直接反应到所引用的实体(变量或对象)。
//对区别9的示例:
int main()
{
int ar[10] = { 12,23,34,34,45,56,9 };
int* ip = &ar[1];
int& ra = ar[1];
++ip;//指针加加 指向ar[2] 指针自身加加
++ra;//引用加加 对元素ar[1]自身的值+1
++(*ip);//解引用在加加 对元素自身加1
return 0;
}
(2)从汇编层面上
调汇编界面查看:汇编中引用与指针初始化角度一样,引用等同于指针,引用从机器码角度上就是指针
(3)两个层面对比
- 从源码角度:指针会分配空间,引用会分配空间
- 从机器码角度:指针与引用都会分配空间
二 、从汇编层简述解释一下引用 ?
编译过程中,将引用变成指针,指针加const做约束[具有常性的指针] (因为引用是固定的 指针加const也就固定)
//[引用与指针在本质上的区别解释]
void func(int& rb)
{
int* ip = &rb;
*ip = 100;
}
int main()
{
int a = 10;
int& ra = a;
func(a);
ra = 200;
return 0;
}
//--->上述代码汇编改写后 变成机器码
//编译过程中 将引用变成指针 加const做约束[具有常性的指针]
//引用从机器码角度上就是指针
void func(int*const rb)//
{
int* ip = rb;//
*ip = 100;
}
int main()
{
int a = 10;
int* const ra = &a;//将引用改成常性指针
func(&a);//
*ra = 200;//
return 0;
}
三、 #define 和const 的区别:
#define和const都可以用来定义常量,但实现方式不同。
-
#define是C/C++预处理器指令,用于定义宏,它在编译时进行文本替换。编译器会将代码中所有用到该宏的地方都替换成宏定义的值。
如果宏定义的值发生变化,所有用到该宏的地方都需要重新编译。
#define还可以用来定义函数宏,可以将一系列语句封装成一个函数式的宏。 -
const是C++中的关键字,用于定义常量。与#define不同,const定义的常量是有类型的,编译器会对其进行类型检查。此外,const定义的常量在程序运行期间是不可修改的,因此更加安全可靠。
使用const定义常量比使用#define更加安全可靠。
在C++中,推荐使用const关键字来定义常量。
四、 C++中 typedef 和 define 的区别:
typedef 和 define 都是 C++ 中用于创建别名的关键字,有以下区别:
- define 是一个预处理指令; typedef 是一个关键字—>类型定义符。
- define 可以用来定义常量、宏和函数等;而 typedef 只能用于定义类型别名。
- define 定义的是文本替换,它在编译前就被处理了;而 typedef 在编译时处理。
- define 定义的标识符没有作用域限制;而 typedef 定义的标识符有作用域限制。
#define PI 3.1415926 // 定义常量
#define MAX(a,b) ((a)>(b)?(a):(b)) // 定义宏
typedef int INT; // 定义类型别名
typedef char *STRING; // 定义指针类型别名
int main()
{
INT i = 10;
STRING str = "hello world";
return 0;
}
五、C++中将引用作为函数的返回值类型的格式~好处和需遵守的规则:
- 好处:可以避免复制大量的数据,提高程序的运行效率;并且可以直接对返回值进行修改。
- 需要遵守的规则如下:
(1) 不可以对函数中的局部变量或对象以引用或指针方式返回。因为局部变量的生命周期只在函数内部,一旦函数执行完毕,这个局部变量就被销毁了,返回它的引用没有意义。
(2)不要返回函数参数的引用,同样也是因为函数执行完毕后,函数参数就会被销毁;
(3)返回引用时要确保被引用的对象存在,并且该对象在函数执行期间不会被销毁。
【引用或指针作为函数的返回值类型】
不可以对函数中的局部变量
变量或者实体的生存期不受函数的影响,就可以返回该变量的地址 (函数结束后 变量仍然存活)
eg.全局变量 或者 静态static修饰函数内的局部变量
函数执行结束 栈内的空间就还给系统
整个程序结束 所有空间都还给系统
//int a=10;
int* func()
{
int a = 10;// 此时a在栈区 不可返回变量的地址
//static int a=10;-->在.data区 可返回该变量地址
return &a;//函数结束后 空间已经还给系统
//变量的生存期影响 static或者全局变量 局部变量 影响变量地址的返回效果
//( 若有新函数add()调用则空间再次被分配,原空间内数据销毁无法查到;若暂时没使用该空间 则还存在原值可查) main函数内add调用与否影响*ip的值
}
void add()
{
int ar[10] = { 1,2,3,4,5,6,7,8,9 };
}
int main()
{
int* ip = func();
//add(); //调用后*ip 为0
printf("*ip =%d\n", *ip); //不调用add(),*ip为10
//函数结束影响变量空间存在与否,变量生存期的因素
return 0;
}
int& func2() //等价于int * const func(){....return &a;}
{
int a = 10; //局部变量 //2: static int a=10;
return a;
}
void add()
{
int ar[10] = { 1,2,3,4,5,6,7,8,9 };
}
int main()
{
int &ra = func2(); //等价于int * const ra=func2();
//add(); //调用后*ip 为0
printf("ra =%d\n", ra); //函数结束后局部变量已销毁 //2: 加static 返回 ra=10;
return 0;
}
六、简述函数的调用过程:
函数的调用过程包括以下几个步骤:
-
函数调用:当程序执行到函数调用语句时,会将当前的执行状态压入栈中,并跳转到函数的代码开始执行处。
-
参数传递:函数调用时,会将实参按照一定的顺序传递给函数。常见的参数传递方式有值传递、引用传递和指针传递等。
-
函数执行:
函数执行时会根据代码逻辑进行计算,并返回一个值(如果有返回值的话)。 -
返回值传递:
函数执行完毕后,会将返回值传递回调用方。常见的返回值传递方式有值返回和引用返回等。 -
函数返回:
函数执行完毕后,会从栈中弹出执行状态,并回到调用函数的位置继续执行。
七、 C++左值、右值、将亡值的概念:
1. 值类别(分三类):
左值:能取地址
亡值(将亡值):在表达式的计算中产生,计算结束时消亡
右值:不能取地址
2. 左值、右值、将亡值的区别:
- 左值:表示一个可以被修改的对象,例如一个变量或者一个数组元素;
- 右值:表示一个不可修改的值,例如常量或者表达式的计算结果;
- 将亡值:聚名之后为左值,取左值引用;不聚名则为右值,取右值引用。
3.引用值类别的好处:
C++11引入了右值引用,允许对右值进行移动语义的操作。通过将右值引用作为函数参数类型或返回类型,可以实现高效的内存管理和避免不必要的拷贝操作。
4.其他补充点:
- 凡是内置类型产生的亡值都具有const常性属性,只可能取值 不可能赋值
eg : a+b=c;编译不通过的原因:a+b产生的亡值有常性不能更改
-
右值类型对自己设置的类型有用处
-
常引用:可读取值,但不具有写性质
常引用是万能引用:既可以引用左值 也可以引用右值 -
非常性左值; 常引用或者普通引用均可;
常性左值 : 只能拿常性左值引用去引用;
int main()
{
int a = 10;//c++ 值类别
char ch='a';//&ch(左值); &'a' (右值)
double dx = 12.23;//&dx (左值) &12.23 (右值)
&a;//能取地址 左值
//&10;//不能取地址 (右值)
return 0;
}
int main()
{
int a = 10;//int ;左值left value
int& b = a;//b int ;左值引用eft ref value
//&10 右值
//int& c = 10; err //int;右值right value
//c++11
int&& c = 10;//&&:右值引用的修饰符 c可以是10的别名
//右值引用的底层操作:
//int temp=10;
//int&& c = temp;
c = 100;
&c;//左值
int& rc = c;//左值引用
return 0;
}
int main()
{
int a = 10;
int& ra = a;//ra=100;
const int& cra = a; //rac==>a 只读 不可写
cout << cra << endl;
a += 10;
cout << cra << endl;
ra += 10;
cout << cra << endl;
//rca += 10;//err 常引用不具有写性质,只能读取数据
cout << cra << endl;
}
八、 简述 __ stdcall和 __ cdecl的区别
0.补充知识点
(1)“C"或"C++”+”函数在内部(编译和链接)通过修饰名识别。
修饰名是编译器在编译函数定义或者原型时生成的字符串。
(2)修饰名由函数名、类名、调用约定、返回值类型、参数表共同决定。
(3)程序运行起来需要经历的阶段:预编译(预处理)、 编译 、汇编、链接、
(4)__stdcall和__cdecl(名字修饰规则中的两种)在编译过程中,将函数名、变量名的名字重新命名的机制。
1. __stdcall和__cdecl
__stdcall和__cdecl都是函数调用约定,用于规定函数的参数传递方式和栈的清理方式。
2.C 语言编译时函数名修饰约定规则
__cdecl是C/C++的缺省调用方式,调用约定函数名字前面添加了下划线前缀。
格式:_functionname
__stdcall调用约定在输出函数名前加上一个下划线前缀,后面加上一个"@"符号和其参数的字节数
格式:_functionname@number
3. __stdcall和__cdecl区别
(1)__stdcall和__cdecl二者区别在于参数的传递顺序和堆栈的清理方式不同。
(2)__stdcall调用约定要求函数的参数从右往左依次入栈,由被调用函数自己负责弹出栈空间。这种调用约定通常用于Windows API函数中。
(3)__cdecl调用约定要求函数的参数从右往左依次入栈,但是由调用方负责清理栈空间。这种调用约定通常用于C/C++的标准库函数中。
九、 C++中const 使用场景
const 关键字用于定义常量。
常量是指一旦被定义就不能再被修改的变量。
使用 const 关键字确保变量不会被无意中修改,提高代码的可读性和安全性。
1. 定义常量:
在定义变量时,使用 const 关键字可以将其声明为常量。
const int MAX = 20260601;
const float PI = 3.14159;
2. 函数中作参数+const:
使用 const 关键字可以指定一个参数为常量参数,这意味着函数不能修改该参数的值。
void print(const int& num)
{
cout << num<< endl;
}
3. 函数返回值+const:
使用 const 关键字可以指定函数返回值为常量,即返回值不能被修改。
const int* getArray()
{
static int arr = {1, 2, 3, 4, 5};
return arr;
}
4. 成员函数:
在类定义中,使用 const 关键字可以指定一个成员函数为常量成员函数,这意味着函数不能修改类的成员变量。
class MyClass
{
public:
int getValue() const
{
return m_value;
}
private:
int m_value;
};
5.const+引用&+指针* 三者的结合 =>auto推导
//练习
int main()
{
int a = 10;
const int* ip = &a;
auto p1 = ip;//p=>const int * ;auto=> const int *
const auto p2 = ip;//p=>const int * ; auto=>int*;
auto& s = ip;//s=>const int* & ;auto=>const int *
int* const sp = &a;
auto p4 = sp; //p2=>int*; auto=>
auto& p3 = sp; //p3=>int * const &; auto=>
}
十、 简述 strcpy、sprintf 与 memcpy 的区别
1.strcpy、sprintf和memcpy的作用
- strcpy:用于将一个字符串复制到另一个字符串中;
- sprintf:将格式化的数据写入一个字符串中;
- memcpy:用于将一个内存地址的数据复制到另一个内存地址。
2.strcpy、sprintf和memcpy语法
- strcpy函数:用于将一个字符串复制到另一个字符串中
char *strcpy(char *dest, const char *src);
dest
为目标字符串的指针,src
为源字符串的指针,返回值为目标字符串的指针。
- sprintf函数:用于将格式化的数据写入到一个字符串中
int sprintf(char *str, const char *format, ...);
str
为目标字符串的指针,format
为格式化字符串,后面可以是一系列要写入的参数。返回值为写入的字符数。
- memcpy函数:该函数用于将一段内存中的数据复制到另一段内存中
void *memcpy(void *dest, const void *src, size_t n);
dest
为目标内存区域的指针,src
为源内存区域的指针,n
为要复制的字节数。返回值为目标内存区域的指针。
3.区别在于使用方法和功能:
- strcpy和memcpy都是复制数据,但是使用方法不同。
strcpy只能复制字符串;
memcpy可以复制任意类型的数据; - sprintf不仅可以复制数据,而且可以格式化输出,将数据以指定的格式写入字符串中。
十一、 简述C++中的指针参数传递和引用参数传递
C++中的指针参数传递和引用参数传递都可以用来在函数中修改函数外部的变量。
1.指针参数传递
(1)指针参数传递是将指向变量的指针作为参数传递给函数,函数内部通过该指针间接操作变量。
(2)指针参数传递的优点是可以在函数内部直接修改指向变量的指针,因此可以实现更复杂的操作,例如动态分配内存等。
2.引用参数传递
(1)引用参数传递是将变量的引用作为参数传递给函数,函数内部通过该引用直接操作变量。
(2)引用参数传递的优点是可以使代码更加简洁易读,因为不需要通过指针间接访问变量。
3.补充点:
如果不需要在函数内部修改指针或者变量的值,应该使用const修饰符来避免意外的修改。
十二、 C++内联函数
1.inline关键字功能
给函数加Inline关键字将其改成内联函数,在编译期间编译器能够在调用点内联展开该函数,编译器会将内联函数的函数体直接嵌入到调用它的代码中,这样可以减少函数调用的开销,提高程序的执行效率。
2.inline要点
(1)inline是一种以空间换时间的做法,省去调用函数额开销。但当函数体的代码过长或者是递归函数即便加上inline关键字,也不会在调用点以内联展开该函数。
(2)inline对于编译器而言只是一个建议,编译器会自动优化。
(3)inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
(4)C++内联函数在编译时进行参数类型检查,这是因为内联函数是在编译时展开的。由于内联函数是在编译时展开的,所以参数类型检查也是在编译时进行的。如果调用内联函数时传递的参数类型与函数定义中声明的参数类型不匹配,编译器就会报错。
(5)内联函数并不是在运行时被执行的,它只是一个编译器的指令,编译器在编译时会将内联函数的代码插入到调用它的地方。因此,内联函数只有在被调用时才会执行,和普通函数一样。
//【inline函数----内联函数】
//在编译期间编译器能在调用点内联展开
//A.h
int add(int x,int y);
//inline int add(int x,int y);
//A.cpp
#include"A.h"
inline int add(int x, int y)
{
return x + y;
}
//test.cpp
#include<iostream>
using namespace std;
#include"A.h"
int main()
{
int x = 10, y = 20, z = 0;
z = add(x, y);
//查汇编 不加inline有现场的保护 现场的维护
//加inline 不进行现场保护维护 直接在调用点内联展开
//inline不建议声明和定义分离,inline被展开就没有函数地址
cout << z << endl;
return 0;
}
十三、 简述栈溢出的原因
-
栈溢出(Stack Overflow)通常是由于函数中使用了过多的局部变量或者递归调用层数过深,导致栈空间不足而引起的。
-
当栈空间耗尽时,程序会继续往栈空间中写入数据,导致覆盖掉了栈底的数据,这就是栈溢出的原因。
-
栈溢出也可能是由于函数调用时传递参数错误、局部变量声明错误等原因导致。一些恶意攻击也可以利用栈溢出漏洞来执行恶意代码。
-
如果程序发生了栈溢出,可能会导致程序崩溃、数据丢失、系统崩溃等严重后果。因此,在编写程序时应该注意避免栈溢出的发生。
十四、 堆区和栈区
1.栈、堆的分配
- 在C++中,内存被划分为两个主要的部分:栈和堆。
- 栈是自动分配的,堆是手动分配的(使用new或malloc)。
- 一般来说,栈区由系统自动管理,而堆区由程序员管理。
2.栈区
- 栈区主要用来存储函数调用时的参数、返回地址、局部变量等,
- 它的大小固定,由系统自动分配和释放。
- 当函数执行完毕后,栈上的数据会自动被删除。
3.堆区
- 堆区则是在程序运行时手动分配和释放的,可以动态地分配内存空间。
- 它的大小不固定,可以根据需要进行分配和释放。
- 由于堆区需要手动管理,因此需要开发人员进行内存管理,防止内存泄漏和内存溢出等问题。
4.二者比较
在性能方面,栈区的操作速度比堆区更快,因为栈区是由系统自动管理的,不需要额外的内存管理。而堆区需要手动进行内存管理,因此会产生额外的开销。
十五、 为什么C++可以重载函数?
1.C++函数重载的名字粉碎
extern外部关键字 ( C/C++命名)
extern "C"
: 函数名以C的方式修饰约定规则
extern "C++"
: 函数名以C++的方式修饰约定规则
//【Windows名字粉碎机制】----解决c++为什么能够实现函数重载 //该机制在Linux下不一样 编译规则不一样
//名字粉碎机制下产生的函数符号修饰名
//函数符号名
//编译阶段 加_functionname@参数字节数
extern "C" int add1(int a);//加"C" 以c的编译方式编译
int add1(int a, int b);
/*
extern "C++" //以C++编译方式编译
{
int add1(int a);//加"C" 以c的编译方式编译
int add1(int a, int b);
}
*/
int main()
{
add1(34);
add1(12, 23);
}
十六、 new, malloc 区别
1.new和malloc都是用来动态分配内存的函数。
最主要的区别是:new是C++中的运算符,而malloc是C中的库函数。
具体区别如下:
- new 和 delete 是 C++ 中的运算符,
而 malloc 和 free 是 C 语言中的库函数。 - new 和 delete 可以执行构造函数和析构函数
而 malloc 和 free 不会执行。 - malloc申请内存空间时,手动计算所需大小
new只需类型名,自动计算大小; - malloc申请的内存空间不会初始化;
new可以初始化; - malloc的返回值为void*,接收时必须强转,new不需要;
- new申请内存空间失败会抛出异常 bad_alloc,要有捕获异常处理程序;malloc 失败时则会返回 NULL,使用时必须判空。
2.三种new的用法
(1)new 关键字调用运算符
//【new delete区别】
// windows 下 动态库.dll 静态库.lib
//C语言的动态内存分配
//C++的动态内存分配 使用new
//【1.new 关键字调用运算符
int main()
{
int n = 10; //在栈区
int* ipa = new int(10);//ipa在栈区,new申请的空间在堆区 ipa里存放堆空间的地址
//new的四步过程:
//1.计算类型大小 //2.调动malloc 在堆区申请大小的空间 //3.用()里的值初始化空间 //4.空间的地址返回
cout <<"ipa:"<<ipa<<" *ipa:"<< * ipa << endl;
*ipa = 30;
cout << "ipa:" << ipa << " *ipa:" << *ipa << endl;
delete ipa; //释放 将ipa指向的堆空间还给系统
ipa = nullptr; //失效指针 赋值空为空指针
ipa = new int(100);
cout << "ipa:" << ipa << " *ipa:" << *ipa << endl;
delete ipa;
ipa = nullptr;
return 0;
}
(2)new函数调用方式
//【new函数调用方式
int main()
{
//new内存申请失败时 抛出异常 == 》throw std::bad_alloc,无需使用if(nullptr==ipa)来判断申请空间成功与否(没必要)
int* ipa = new int(10);//new关键字调用
delete ipa;//关键字释放
ipa = nullptr;
//1.计算申请空间 //2.底层调用malloc //3.返回值要类型强转 //4.空间没有初始化
int* ipb = (int*)::operator new(sizeof(int));//函数形式调用new
::operator delete(ipb);//函数方式释放
ipb = nullptr;
//不让系统抛出异常的操作 若申请失败则返回0
int* ipc = new(std::nothrow) int(10);//此时可以使用if(nullptr==ipa)判断
::operator delete(ipc);
if (nullptr == ipc);
ipc = nullptr;
/*int* p = (int*)malloc(sizeof(int)); //malloc内存申请失败返回空值
if (nullptr == p);
free(p);
p=nullptr;*/
}
//使用初始化任何类型的变量
int main()
{
int n = 10;
int x = 20;
//new 申请的空间 指针不压轻易移动 避免delete出错
//free 与delete 释放空间时:系统会自动添加 :头部信息、上越界标记(28+4)、下越界标记(4)
int* ipa = new int(x);
int* ipb = new int{x};
int* ipc = new int[n] {x};//1.计算类型的大小 申请sizeof(int)*n //2.调动malloc 在堆区申请大小的空间 //3.用{x}初始化一个整型空间 未初始化的赋初值为0 //4.空间的地址返回
int* ipd = new int[n] {1,2,3,4,5,6,7,8,9,10};//申请的空间用{......}赋值
cout << *ipa << endl;
cout << *ipb << endl;
//for (int i = 0; i < n; i++)
//{
// cout << *ipc << endl;
// ipc++;//指针移动了
//}
delete ipa; ipa = nullptr;
delete ipb; ipb = nullptr;
//delete[]ipc; ipc = nullptr;
return 0;
}
int main()
{
int n = 10;
int* ip = new int[n] {12, 23, 34, 45, 56, 67, 78, 89};
for (int i = 0; i < n; i++)
{
cout << ip[i] << " ";
}
delete[] ip;
ip = nullptr;
return 0;
}
(3)定位new
//【定位new ---》 只在乎ip是否有空间 可以用任意类型初始化该空间
int main()
{
int* ip = (int*)::operator new(sizeof(int));
new(ip)int(10); //定位new //new (ip) int(10);
int* ip = new int(10);
cout << *ip << endl;
::operator delete(ip);
ip = nullptr;
}
int main()
{
char buff[128];
new(buff) int[] {1, 2, 3, 4, 5};//将buff空间 以整型数组初始化
new(buff) float(12.25); // 将buff空间 以浮点型初始化
}
十七、time函数 clock 函数的区别
time函数和clock函数都是用来测量程序执行时间的函数。
1.time函数
#include <time.h>
time_t time(time_t *time);
time函数返回自1970年1月1日以来经过的秒数,如果参数time非空,则将结果存储在time指向的内存中。
2.clock函数
#include <time.h>
clock_t clock(void);
clock函数返回程序执行起点(一般是程序的入口)到当前所用CPU时间,单位为时钟周期。可以通过CLOCKS_PER_SEC常量将时钟周期转换成秒数。
3.区别
- time函数和clock函数二者实现方式和返回值不同。
(1)time函数返回的是从1970年1月1日到当前时间的秒数,可以用于计算时间差。
(2)而clock函数返回的是程序运行时间的时钟数,单位是毫秒,可以用于测量程序中某一段代码的执行时间。
(3)补充:clock函数返回的时钟数是CPU时钟数,在多核CPU上运行时,clock函数返回的值可能会受到其他进程或线程的影响。