输入输出的区别
#include<stdio.h>
#include<iostream>
using namespace std;
int main()
{
int a;
char ch;
scanf_s("%d %c",&a,&ch);
printf("%d %c",a,ch);
cin >> a >> ch;
cout << a <<" "<< ch << endl;
return 0;
}
引用
引用的定义
类型& 引用变量名称 = 变量名称; 这就是引用变量的定义,&和类型结合称之为引用符号,不是取地址的符,代表别名的意思。
&符号在c/c++中的应用 引用相当于给变量起一个外号,例如 给张磊起一个外号叫磊子,而张磊与磊子指的是一个人,即是一个实体
函数并不是通用该方式起一个别命,而是我们这个函数返回了一个引用,上面的只是参数为引用类型
引用的概念
- 定义引用必须给予初始化
- 不能有所谓的空引用
- 没有所谓的引用的引用
我们可以认为a就是x,x就是a 以上都是错误的,引用不能为空,且没有分级,这一点与指针是不同的 a与b有相同的地址,且操作a就相当于操作b
而下面的代码,我们新添加了一个 const int& c = a; 这样使得我们可以通过a的修改来改变c,但是不能修改c,也就是能力收缩
int main()
{
int a = 10;
int& b = a;
const int& c = a;
b += 10;
a += 10;
}
我们再来看下面这一段代码
int main()
{
const int a = 10;
int &b = a;
}
这里的代码是错误的,不能编译通过,a自身不能进行修改,而若可以通过b进行修改而使得a修改,则出现了二义性
我们可以使能力收缩,而不能使能力扩展;第一段代码中 c 属于能力收缩,而第二段代码中 b 属于能力扩展
引用的使用
引用在使用时比指针要方便的多,例如以下代码,像Swap_err 中我们a与b的交换并没有影响实参,而下面的Swap 中定义引用,且函数中这样写并不与前面所讲的引用必须给予初始化矛盾因为函数在未调用时引用并不存在,只有在调用时,才会让y初始化b,用x初始化a,这样是真正的对x,y数值进行交换
void Swap_err(int a, int b)
{
int tmp = a;
a = b;
b = tmp;
}
void Swap(int &a,int &b)
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int x = 10, y =20;
Swap(x,y);
}
inline函数 内联函数
再C/C++中,为了解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题,特别的引入了inline修饰符,表示未内联函数。
inline int Max(int a, int b)
{
return a + b;
}
内联函数的处理方式是在函数的调用点直接代码展开,在计算机系统下,假如频繁的调用就会造成较大的时间开销(现场保护、现场恢复、栈帧开辟与清除);内联函数的引入减少了函数调用过程中开栈和清栈的开销,处理流程如下: 内联函数与普通函数的区别
- 内联函数在调用点代码直接展开,没有开栈与清栈的开销;普通函数有开栈与清栈的开销
- 内联函数体要求代码简单,不能包含复杂的结构控制语句
- 如果内联函数函数体过于复杂,编辑器会自动把内联函数当成普通函数来执行
内联函数与static修饰的函数的区别
- static修饰的函数处理机制只是将函数符号变成局部符号,函数的处理机制和普通函数相同,都有函数的开栈与清栈开销;而内联函数没有函数的开栈与清栈
- inline函数是因为代码在调用点直接展开导致函数本文件可见;而static修饰的函数是因为函数符号从全局符号变成了局部符号导致函数本文本可见
内联函数与宏的区别
- inline函数的处理时机实在编译阶段处理的,有安全检查和类型检查;而宏的处理实在预编译阶段处理,没有任何检查机制,只是简单的文本替换
- inline函数是一种更安全的宏
我们写这样一个函数通过汇编来查看,这里是通过call来调用函数 如果在函数上加上inline,发现依旧还是函数调用,这是因为inline函数只能在Release版本生效,在Debug版本是不生效的 在Release版本,直接将值计算出来
类比如
c = ADD_Int(a,b);
c = 10 + 20;
如果内联函数存在判断语句或者循环语句,或者行数过大,编译器会认为它不应该是一个内联函数
函数参数的默认值
void fun(int a,int b,int c = 0,int d = 0, int e = 0)
{}
int main()
{
fun(12,23,34);
}
默认值在编译过程中确定,当我们发现该值没有实参输入时,会将默认值入栈,如果有实参值会将实参进行替换 改结果输出为:12 45 56 0 0 在这里逗号表达式最终确定的值为45
函数的重载
extern"C" int Max(int a, int b)
{
return a > b ? a : b;
}
extern"C" int fun(int a, int b)
{
return a + b;
}
int main()
{
Max(10,20);
fun(20,30);
return 0;
}
extern 关键字 代表本文件用,其他文件也想用 extern “C” 关键字 代表使用C的方式进行编译 这是按照C方式编译
int Max(int a, int b)
{
return a > b ? a : b;
}
int fun(int a, int b)
{
return a + b;
}
int main()
{
Max(10,20);
fun(20,30);
return 0;
}
C++在区分函数并是靠函数名,而是靠函数原型(函数返回类型 + 函数名 + 形参列表,类型,个数)
C++编译没有下划线 我们写另外几个类型查看
int Max(int a, int b)
{
return a > b ? a : b;
}
double Max(double a, double b)
{
return a > b ? a : b;
}
char Max(char a, char b)
{
return a > b ? a : b;
}
int main()
{
Max(10,20);
Max('a','b');
Max(12.23,34.45);
return 0;
}
在这里依旧并没有看到命名规则,这里需要使用vc查看,vs2019会将其进行舍去 通过vc我们看到这三个函数的命名规则
名字粉碎
在C语言中对函数的命名比较简单,仅仅是对函数名前加一个 “_” 而C++会对函数进行名字粉碎,首先我们名字约定为: @@YG 代表的是调用约定,即stdcall回调方式,如果使用C的方式cdecl 我们来查看一下
int __stdcall Max(int a, int b)
{
return a > b ? a : b;
}
double __cdecl Max(double a, double b)
{
return a > b ? a : b;
}
char Max(char a, char b)
{
return a > b ? a : b;
}
int main()
{
Max(10,20);
Max('a','b');
Max(12.23,34.45);
return 0;
}
在进行编译查看 可以看到 ?开头,后面跟着 Max,然后是 @@YA (回调方式)以及 @@YG (C调用约定) 随后在主函数中,编译器在编译时会对函数进行重新命名
int main()
{
Max(10,20);
Max('a','b');
}
这也就是为什么C++可以进行函数的重载,而C不可以
- C语言编译过程,命名规范是给函数名前加一个下划线,导致不能区分函数
- C++进行名字粉碎,对函数重命名
调用二义性
尽管在名字上可以区分,但是我们在调用时候,无论调用哪一个函数都是可以的,继而出现了调用二义性,所以会发生错误
继而返回类型不能作为函数重载的依据
void fun(int a, int b)
{
printf("2\n");
}
void fun(int a, int b, int c = 0)
{
printf("3\n");
}
int main()
{
fun(12,23);
fun(12,23,34);
return 0;
}
这样同样是错误的,因为存在了调用二义性
模板函数
函数模板不是一个实在的函数,编译器不能为其生成可执行代码。定义函数模板后只是一个对函数功能框架的描述,当它具体执行时,将根据传递的实际参数决定其功能。例子如下:
#include<stdio.h>
#include<iostream>
using namespace std;
struct Student
{
int s_num;
int s_age;
};
template<typename Type>
void Swap(Type& a, Type& b)
{
Type tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 10,b = 20;
double da = 12.23,db = 23.34;
char ch1 = 'a',ch2 = 'b';
struct Student s1 = {202001,23},s2 = {202002,19};
Swap(a,b);
Swap(da,db);
Swap(ch1,ch2);
Swap(s1,s2);
}
在发现a,b为int类型 会重命名生成整型类型,而遇到da,db同样会生成double类型
模板是在编译过程中生成代码,运行只是根据类型进行不同代码进行挂接
模板重命名与宏是不同的
空间申请
new/delete 和 malloc/free 区别 在C++中堆内存的分配和释放是通过new和delete来操作的。使用方式如下: 开辟内存:类型* 指针变量名 = new 类型(初始化数值);//自由区//.heap 释放内存: delete 指针变量名;
int n = 10;
int *p = (int*)malloc(sizeof(int)*n);
if(ip == NULL) exit(0);
free(ip);
ip = NULL;
ip = new int;
*ip= 100;
delete ip;
ip = NULL;
ip = new int[n];
delete[]ip;
ip = NULL;
- malloc 申请空间,如果内存不足,会给ip置空
- new 申请连续空间,空间不足申请失败,并不会给ip置空,而是会抛出一个异常;或者
ip = new(nothrow) int[n]; 该方式会将ip置空
面向过程与面向对象
面向过程
当软件超过一定尺度后,采用结构化程序设计,其开发和维护就越来越难控制;其根本的原因在于面向过程的结构化程序设计的方法与现实世界(包括主观世界和客观世界)往往都不一样,结构化程序设计的思想往往很难贯彻到底
在结构化程序设计中,采用的是“自顶向下,逐步细化”的思想;具体操作方法是模块化,是按照功能来分的,所以也称功能块(函数);在C++中成为一个函数,一个函数解决一个问题,即实现一个功能或一个操作
在模块化的思想中已经出现了封装的概念,这个封装是把数据封装到模块中,即局部变量;但这是很不彻底的,因为模块是功能的抽象,而数据则是具有其个性的,一旦发生那怕是一点变化,抽象的功能模块就不在适用了;可维护性差成了制约结构化程序设计应用的瓶颈
面向对象
对象的概念是面向对象技术的核心所在,面向对象技术中的对象就是现实世界中某个具体的物理实体在计算机逻辑中的映射和体现 对主观实体进行分析得到抽象类别,对主观世界进行设计得到类,对类实例化得到对象 根据已存在的房子(实体)进行分析和抽象,得到自己需求的设想(概念世界),对设想进行设计成图纸(类),将图纸实例化成房子(对象) 使用计算机程序去模拟现实世界的物理存在,这就是C++的根本思想
总结:
- 类是一组相关的属性(变量)和 行为(方法)的集合,是一个抽象概念的设计的产物
- 对象是该类事物的具体表现形式,具体存在的实体
- 成员变量是对象的属性(可以是变量、指针、数组等),属性的值确定对象的状态
- 成员函数是对象的方法,确定对象的行为
状态和行为是对象的主要属性
- 对象的状态又称为对象的静态属性,主要指对象内部所包含的各种信息,也就是个变量;每个对象个体都有自己专有的内部变量,这些变量的值标明了对象所处的状态
- 对象的方法(行为)一方面把对象的内部变量包裹、封装、保护起来,使得只有对象自己的方法才能操作这些内部变量,另一方面对象的方法还是对象与外部环境的其他对象交互,通信的接口,对象的环境和其他对象可以通过这个接口来调用对象的方法,操作对象的行为和改变对象的状态
- 对象是现实世界的实体或概念的计算机逻辑中的抽象表示;具体地,对象是具有唯一对象名和固定对外界接口的一组属性和操作的集合,用了模拟或影响现实世界问题的一个或一组因素
类和对象
封装是面向对象程序设计的最基础的特性,把数据(属性)和函数(操作)合成一个整体,在这计算机世界中是用类与对象实现的。
class CGoods
{
public:
char Name[21];
int Amount;
float Price;
float Total_value;
};
int main()
{
int a = 10;
CGoods c1;
c1.Amount = 10;
c1.Price = 10;
}
关键字class是数据类型说明符,指出下面说明的是类;标识符CGoods是商品这个类的类型名;花括号中是构成类体的一系列的成员,关键字public是一种访问限定符
访问限定符有三种:public(共有的),private(私有的),protected(保护的);第一种说明的成员能从外部进行访问,另外两种说明的成员不能从外部进行访问;每种说明符可在类体中使用多次,他们的作用域是从该说明符出现开始到下一个说明符之前或类体结束之前结束
如果类体起始点无访问说明符,系统默认定义为私有(private);访问说明符private(私有的)和protected(保护的)体现了类具有封装性
类性设计的更关键部分是对数据成员的操作,用函数来完成:
class CGoods
{
private:
char Name[21];
int Amount;
float Price;
float Total_value;
public:
void RegisterGoods(const char *,int,float);
void CountTotal(void);
void GetName(char[]);
int GetAmount(void);
float GetPrice(void);
float GetTotal_value(void);
};
void CGoods::RegisterGoods(const char *name, int amount, float price)
{
strcpy(Name, name);
Amount = amount;
Price = price;
}
void CGoods::CountTotal(void)
{
Total_value = Price * Amount;
}
void CGoods::GetName(char name[])
{
strcpy(name, Name);
}
int CGoods::GetAmount(void)
{
return Amount;
}
float CGoods::GetPrice(void)
{
return Price;
}
上面只有方法是声明,而没有方法的定义;方法的定义可以在类内也可以在类外
四个数据成员被说明成私有,而六个函数成员被说明成共有;也就是说如果从外部对四个数据成员进行操作的话,只能通过六个公有函数来完成,数据收到了良好的保护,不易受副作用的影响;共有函数集定义了类的接口
类是一种数据类型,定义时系统不为类分配存储空间,所以不能对类的数据成员初始化;类中的任何数据成员也不能使用关键字extern、auto或register限定其存储类型
成员函数可以直接使用类定义中的任一成员,可以处理数据成员,也可以调用函数成员
class CGoods
{
private:
char Name[21];
int Amount;
float Price;
float Total_value;
public:
void RegisterGoods(const char *,int,float);
void CountTotal(void);
void GetName(char[]);
int GetAmount(void);
float GetPrice(void);
float GetTotal_value(void)
{
return Total_value;
}
};
void CGoods::CountTotal(void) ,这四个点代表的是作用域解析符,如果没有它void CountTotal(void) 我们会认为这是一个全局函数,当我们需要让它成为类中某一个函数的定义;继而需要同返回类型、同函数名、相同的形参列表、以及需要在函数名前加上类型名和作用域解析符
int main()
{
CGoods c1, c2, c3;
c1.RegisterGoods("c++", 12, 98.99);
c1.CountTotal();
c2.RegisterGoods("java", 23, 12.23);
c2.CountTotal();
c3.RegisterGoods("tea", 23, 34);
c3.CountTotal();
}
我们创建了三个对象,若每个对象都存储了数据与方法,那么当对象数量较多时,内存将无法承载 但是如果这样的话,我们的代码如何区分数据到底来自c1还是c2? 这个问题我们在后面的博客进行详细解决
|