文章目录
内联函数的引出——宏缺陷
c++从c中继承的一个重要特征就是效率。假如c++的效率明显低于c的效率,那么就会有很大的一批程序员不去使用c++了。
在c中我们经常把一些短并且执行频繁的计算写成宏,而不是函数,这样做的理由是为了执行效率,宏可以避免函数调用的开销,这些都由预处理来完成。
但是在c++出现之后,使用预处理宏会出现两个问题:
1.第一个在c中也会出现,宏看起来像一个函数调用,但是会有隐藏一些难以发现的错误。
2.第二个问题是c++特有的,预处理器不允许访问类的成员,也就是说预处理器宏不能用作类类的成员函数。
为了保持预处理宏的效率又增加安全性,而且还能像一般成员函数那样可以在类里访问自如,c++引入了内联函数(inline function).
内联函数为了继承宏函数的效率,没有函数调用时开销,然后又可以像普通函数那样,可以进行参数,返回值类型的安全检查,又可以作为成员函数。
在普通函数(非成员函数)前面加上inline使之成为内联函数,要注意函数声明和函数体需要结合在一起,否则编译器将它作为内联函数对待。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
//定义一个加法
#define MyAdd(x,y) ((x)+(y))
void test01()
{
int ret = MyAdd(10, 20) *20; //预期结果 600 ((10)+(20))*20
cout << "ret = " << ret << endl;
}
#define MyCompare(a,b) ((a) < (b)) ? (a) :(b)
inline void mycompare(int a, int b)
{
int ret = a < b ? a : b;
cout << "ret ::::: " << ret << endl;
}
//1 内联函数注意事项
// 类内部的成员函数 默认前面会加inline关键字
inline void func(); //内联函数声明
inline void func() { }; //如果函数实现时候,没有加inline关键字 ,那么这个函数依然不算内联函数
void test02()
{
int a = 10;
int b = 20;
//int ret = MyCompare(++a, b); // 预期结果 11 ((++a) < (b)) ? (++a):(b),但结果是12
//cout << "ret = " << ret << endl;
mycompare(++a, b);//预处理的时候就会将函数体的内容将此语句替换。
}
//3 宏函数也没有作用域
int main(){
//test01();
test02();
system("pause");
return EXIT_SUCCESS;
}
inline void func(int a);
以上写法没有任何效果,仅仅是声明函数,应该如下方式来做:
inline int func(int a){return ++;}
注意: 编译器将会检查函数参数列表使用是否正确,并返回值(进行必要的转换)。这些事预处理器无法完成的。
内联函数的确占用空间,但是内联函数相对于普通函数的优势只是省去了函数调用时候的压栈,跳转,返回的开销。我们可以理解为内联函数是以空间换时间。
类内部的内联函数
为了定义内联函数,通常必须在函数定义前面放一个inline关键字。但是在类内部定义内联函数时并不是必须的。任何在类内部定义的函数自动成为内联函数。
class Person{
public:
Person(){ cout << "构造函数!" << endl; }
void PrintPerson(){ cout << "输出Person!" << endl; }
}
构造函数Person,成员函数PrintPerson在类的内部定义,自动成为内联函数。
inline的基本使用
在C++中内联成员函数与非内联成员函数的可以分为两种情况:
1.如果成员函数的声明和定义是在一起的,那么无论有没有写inline这个成员函数都是内联的,如下:
using namespace std;
class test{
public:
void fuc() {
cout << "ok!" << endl;
}
};
int main(void)
{
test t, t1;
t.fuc();
t1.fuc();
return 0;
}
2.如果成员函数的声明和定义是分开的,那么如果两者中有一个加上了inline都会使成员函数都是内联的,如:
#include <iostream>
using namespace std;
class test{
public:
inline void fuc();
};
int main(void)
{
test t, t1;
t.fuc();
t1.fuc();
return 0;
}
void test::fuc(){
cout << "ok!" << endl;
}
要想定义非内联成员函数,只有一种方法即:声明和定义都不加inline,如下
#include <iostream>
using namespace std;
class test{
public:
void fuc();
};
int main(void)
{
test t, t1;
t.fuc();
t1.fuc();
return 0;
}
void test::fuc(){
cout << "ok!" << endl;
}
内联函数和编译器
内联函数并不是何时何地都有效,为了理解内联函数何时有效,应该要知道编译器碰到内联函数会怎么处理?
对于任何类型的函数,编译器会将函数类型(包括函数名字,参数类型,返回值类型)放入到符号表中。同样,当编译器看到内联函数,并且对内联函数体进行分析没有发现错误时,也会将内联函数放入符号表。
当调用一个内联函数的时候,编译器首先确保传入参数类型是正确匹配的,或者如果类型不正完全匹配,但是可以将其转换为正确类型,并且返回值在目标表达式里匹配正确类型,或者可以转换为目标类型,内联函数就会直接替换函数调用,这就消除了函数调用的开销。假如内联函数是成员函数,对象this指针也会被放入合适位置。
类型检查和类型转换、包括在合适位置放入对象this指针这些都是预处理器不能完成的。
但是c++内联编译会有一些限制,以下情况编译器可能考虑不会将函数进行内联编译:
不能存在任何形式的循环语句
不能存在过多的条件判断语句
函数体不能过于庞大
不能对函数进行取址操作
内联仅仅只是给编译器一个建议,编译器不一定会接受这种建议,如果你没有将函数声明为内联函数,那么编译器也可能将此函数做内联编译。一个好的编译器将会内联小的、简单的函数。
函数的默认参数以及占位参数
默认参数
c++在声明函数原型的时可为一个或者多个参数指定默认(缺省)的参数值,当函数调用的时候如果没有指定这个值,编译器会自动用默认值代替。
void TestFunc01(int a = 10, int b = 20){
cout << "a + b = " << a + b << endl;
}
//注意点:
//1. 形参b设置默认参数值,那么后面位置的形参c也需要设置默认参数
void TestFunc02(int a,int b = 10,int c = 10){}
//2. 如果函数声明和函数定义分开,函数声明设置了默认参数,函数定义不能再设置默认参数
void TestFunc03(int a = 0,int b = 0);
void TestFunc03(int a, int b){}
int main(){
//1.如果没有传参数,那么使用默认参数
TestFunc01();
//2. 如果传一个参数,那么第二个参数使用默认参数
TestFunc01(100);
//3. 如果传入两个参数,那么两个参数都使用我们传入的参数
TestFunc01(100, 200);
return EXIT_SUCCESS;
}
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
//函数的默认参数 参数后面 = ...
//函数参数注意事项,如果有一个位置有了默认参数,那么从这个位置开始,从左往后都必须有默认参数
// 传入参数 ,如果有参数,就用传入的参数,没有参数就用默认值
//如果函数声明里面有了 默认参数,那么函数实现时候必须没有
// 函数声明和实现里 只能有一个里有默认参数,不要同时都出现默认参数
void myFunc(int a = 10, int b = 10);
void myFunc(int a , int b ){}
void func( int a , int b = 10,int c = 1 )
{
cout << "a + b + c = " << a + b + c << endl;
}
void test01()
{
//func();
func(1,2);
}
//函数 占位参数
//如果有了占位参数,函数调用时候必须要提供这个参数 ,但是用不到参数
//占位参数 没有什么大用途,只有后面重载 ++符号才有一点点用
//占位参数 可以有默认值
void func2(int a , int = 1)
{
}
void test02()
{
func2(10);
}
int main(){
test01();
system("pause");
return EXIT_SUCCESS;
}
占位参数
c++在声明函数时,可以设置占位参数。占位参数只有参数类型声明,而没有参数名声明。一般情况下,在函数体内部无法使用占位参数。
函数重载
在传统c语言中,函数名必须是唯一的,程序中不允许出现同名的函数。在c++中是允许出现同名的函数,这种现象称为函数重载。函数重载的目的就是为了方便的使用函数名。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
//函数重载
// C++中 函数名称可以重复
// 必须在同一个作用域,函数名称相同
// 函数的参数 个数不同 或者 类型不同 或者 顺序不同
void func()
{
cout << "无参数的func" << endl;
}
void func(int a )
{
cout << "有参数的func(int a)" << endl;
}
void func(double a)
{
cout << "有参数的func(double a)" << endl;
}
void func(double a , int b)
{
cout << "有参数的func(double a ,int b)" << endl;
}
void func(int a, double b)
{
cout << "有参数的func(int a ,double b)" << endl;
}
//返回值可以作为函数重载的条件吗??? 不可以
//int func(int a, double b)
//{
// cout << "有参数的func(int a ,double b)" << endl;
// return 1;
//}
void test01()
{
//func(1.1,3);
func(1, 3.14);
}
//当函数重载 碰到了 默认参数时候,要注意避免二义性问题
void func2(int a,int b = 10)
{
}
void func2(int a)
{
}
void test02()
{
//func2(10);
}
//引用的重载版本
void func3(int &a) //引用必须要引合法的内存空间
{
cout << " int &a" << endl;
}
void func3(const int &a) //const也是可以作为重载的条件 int tmp = 10; const int &a = tmp;
{
cout << "const int &a" << endl;
}
void test03()
{
//int a = 10;
func3(10);
}
int main(){
//test01();
//test03();
system("pause");
return EXIT_SUCCESS;
}
函数重载的实现原理
编译器为了实现函数重载,也是默认为我们做了一些幕后的工作,编译器用不同的参数类型来修饰不同的函数名,比如void func(); 编译器可能会将函数名修饰成_func,当编译器碰到void func(int x),编译器可能将函数名修饰为_func_int,当编译器碰到void func(int x,char c),编译器可能会将函数名修饰为_func_int_char我这里使用”可能”这个字眼是因为编译器如何修饰重载的函数名称并没有一个统一的标准,所以不同的编译器可能会产生不同的内部名。
void func(){}
void func(int x){}
void func(int x,char y){}
以上三个函数在linux下生成的编译之后的函数名为:
_Z4funcv //v 代表void,无参数
_Z4funci //i 代表参数为int类型
_Z4funcic //i 代表第一个参数为int类型,第二个参数为char类型
extern "C"浅析
以下在Linux下测试:
c函数: void MyFunc(){} ,被编译成函数: MyFunc
c++函数: void MyFunc(){},被编译成函数: _Z6Myfuncv
通过这个测试,由于c++中需要支持函数重载,所以c和c++中对同一个函数经过编译后生成的函数名是不相同的,这就导致了一个问题,如果在c++中调用一个使用c语言编写模块中的某个函数,那么c++是根据c++的名称修饰方式来查找并链接这个函数,那么就会发生链接错误,以上例,c++中调用MyFunc函数,在链接阶段会去找Z6Myfuncv,结果是没有找到的,因为这个MyFunc函数是c语言编写的,生成的符号是MyFunc。
那么如果我想在c++调用c的函数怎么办?
extern "C"的主要作用就是为了实现c++代码能够调用其他c语言代码。加上extern "C"后,这部分代码编译器按c语言的方式进行编译和链接,而不是按c++的方式。
test.h
#pragma once
#ifdef __cplusplus //两个_下划线
extern "C" {
#endif // !__cplusplus
#include <stdio.h>
void show();
#ifdef __cplusplus //两个_下划线
}
#endif // !__cplusplus
test.c
#include "test.h"
void show()
{
printf("hello world \n");
}
extern C 浅析.cpp
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
#include "test.h"
//C++中想调用C语言方法
//extern "C" void show(); //show方法 按照C语言方式做连接
//解决的问题就是 在C++中调用C语言的函数
int main(){
show(); //在C++中 函数是可以发生重载的,编译器会把这个函数名称偷偷改变 _showv void
system("pause");
return EXIT_SUCCESS;
}
C语言下的封装和C++语言下的封装
c语言下的封装.c
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
struct Person
{
char mName[64];
int mAge;
};
void PersonEat(struct Person *p)
{
printf("%s 在吃人饭 \n",p->mName);
}
void test01()
{
struct Person p1;
strcpy(p1.mName, "德玛西亚");
PersonEat(&p1);
}
struct Dog
{
char mName[64];
int mAge;
};
void DogEat(struct Dog * dog)
{
printf("%s 在吃狗粮 \n", dog->mName);
}
void test02()
{
struct Dog d;
strcpy(d.mName, "旺财");
DogEat(&d);
struct Person p1;
strcpy(p1.mName, "老王");
DogEat(&p1);
}
//C语言封装 属性和行为分开处理了 ,类型检测不够
int main(){
//test01();
test02();
system("pause");
return EXIT_SUCCESS;
}
C++语言下的封装.cpp
public 类内 类外 都可以访问
protected 类内可以访问 类外 不可以访问 (子类可以访问)
private 类内可以访问 类外 不可以访问 (子类不可以访问)
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
struct Person
{
char mName[64];
int mAge;
void PersonEat()
{
cout << mName <<"吃人饭" << endl;
}
};
struct Dog
{
char mName[64];
int mAge;
void DogEat()
{
cout << mName << "吃狗粮" << endl;
}
};
//C++中的封装 严格类型转换检测, 让属性和行为 绑定到一起
// 1 属性和行为作为一个整体来表示生活中的事物
// 2 控制权限 public 公有权限 protected 保护权限 private 私有权限
void test01()
{
Person p1;
strcpy(p1.mName, "老王");
p1.PersonEat();
//p1.DogEat();//出错,“DogEat”不是“Person”的成员。
}
//struct 和class是一个意思,唯一的不同 默认权限 ,struct是public,但是class默认权限是private
class Animal
{
private:
//如果我不声明权限,默认的权限是 private
void eat(){ mAge = 100; };
int mAge;
public:
int mHeight;
protected: //保护权限 类内部可以访问 ,(当前类的子类可以访问) , 类外部不可以访问
int mWeight;
void setWeight(){ mWeight = 100; };
};
//所谓私有权限 就是私有成员(属性、函数) 在类内部可以访问,类外部不可以访问
//公共权限 ,在类内部和类外部都可以访问
void test02()
{
Animal an;
//an.eat();
//an.mAge; //私有不可以访问到
an.mHeight = 100; //公有权限在类外部可以访问到
//an.mWeight = 100; //保护权限 在类外不可访问到
}
// public 类内 类外 都可以访问
// protected 类内可以访问 类外 不可以访问 (子类可以访问)
// private 类内可以访问 类外 不可以访问 (子类不可以访问)
int main(){
test01();
system("pause");
return EXIT_SUCCESS;
}
定义一个结构体用来表示一个对象所包含的属性,函数用来表示一个对象所具有的行为,这样我们就表示出来一个事物,在c语言中,行为和属性是分开的,也就是说吃饭这个属性不属于某类对象,而属于所有的共同的数据,所以不单单是PeopleEat可以调用Person数据,AnimalEat也可以调用Person数据,那么万一调用错误,将会导致问题发生。
从这个案例我们应该可以体会到,属性和行为应该放在一起,一起表示一个具有属性和行为的对象。
假如某对象的某项属性不想被外界获知,比如说漂亮女孩的年龄不想被其他人知道,那么年龄这条属性应该作为女孩自己知道的属性;或者女孩的某些行为不想让外界知道,只需要自己知道就可以。那么这种情况下,封装应该再提供一种机制能够给属性和行为的访问权限控制住。
所以说封装特性包含两个方面,一个是属性和变量合成一个整体,一个是给属性和函数增加访问权限。
封装
- 把变量(属性)和函数(操作)合成一个整体,封装在一个类中
- 对变量和函数进行访问控制
访问权限
1.在类的内部(作用域范围内),没有访问权限之分,所有成员可以相互访问
2.在类的外部(作用域范围外),访问权限才有意义:public,private,protected
3.在类的外部,只有public修饰的成员才能被访问,在没有涉及继承与派生时, private和protected是同等级的,外部不允许访问
访问属性 | 属性 | 对象内部 | 对象外部 |
---|---|---|---|
public | 公有 | 可访问 | 可访问 |
private | 私有 | 可访问 | 不可访问 |
protected | 保护 | 可访问 | 不可访问 |
C++数据封装
所有的 C++ 程序都有以下两个基本要素:
程序语句(代码):这是程序中执行动作的部分,它们被称为函数。
程序数据:数据是程序的信息,会受到程序函数的影响。
封装是面向对象编程中的把数据和操作数据的函数绑定在一起的一个概念,这样能避免受到外界的干扰和误用,从而确保了安全。数据封装引申出了另一个重要的 OOP 概念,即数据隐藏。
数据封装是一种把数据和操作数据的函数捆绑在一起的机制,数据抽象是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制。
C++ 通过创建类来支持封装和数据隐藏(public、protected、private)。我们已经知道,类包含私有成员(private)、保护成员(protected)和公有成员(public)成员。默认情况下,在类中定义的所有项目都是私有的。例如:
class Box
{
public:
double getVolume(void)
{
return length * breadth * height;
}
private:
double length; // 长度
double breadth; // 宽度
double height; // 高度
};
变量 length、breadth 和 height 都是私有的(private)。这意味着它们只能被 Box 类中的其他成员访问,而不能被程序中其他部分访问。这是实现封装的一种方式。
为了使类中的成员变成公有的(即,程序中的其他部分也能访问),必须在这些成员前使用 public 关键字进行声明。所有定义在 public 标识符后边的变量或函数可以被程序中所有其他的函数访问。
把一个类定义为另一个类的友元类,会暴露实现细节,从而降低了封装性。理想的做法是尽可能地对外隐藏每个类的实现细节。
数据封装的实例
C++ 程序中,任何带有公有和私有成员的类都可以作为数据封装和数据抽象的实例。请看下面的实例:
#include <iostream>
using namespace std;
class Adder{
public:
// 构造函数
Adder(int i = 0)
{
total = i;
}
// 对外的接口
void addNum(int number)
{
total += number;
}
// 对外的接口
int getTotal()
{
return total;
};
private:
// 对外隐藏的数据
int total;
};
int main( )
{
Adder a;
a.addNum(10);
a.addNum(20);
a.addNum(30);
cout << "Total " << a.getTotal() <<endl;
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Total 60
上面的类把数字相加,并返回总和。公有成员 addNum 和 getTotal 是对外的接口,用户需要知道它们以便使用类。私有成员 total 是对外隐藏的,用户不需要了解它,但它又是类能正常工作所必需的。
建议将成员属性设置为私有
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include <string>
using namespace std;
class Person
{
public:
//设置年龄
void setAge(int age)
{
if (age < 0 || age > 100)
{
cout << "你这个老妖精" << endl;
return;
}
m_Age = age;
}
//获取年龄 读权限
int getAge()
{
return m_Age;
}
//读姓名
string getName()
{
return m_Name;
}
//写姓名
void setName(string name)
{
m_Name = name;
}
//只写的情人
void setLover(string lover)
{
m_lover = lover;
}
private: //类外不可访问,类内可以访问
int m_Age = 0; //年龄 读写
string m_Name; //公有权限 读写
string m_lover; //情人 只写
};
void test01()
{
Person p1;
p1.setName("老王");
cout << "p1的姓名:" << p1.getName() << endl;
//年龄
p1.setAge(120);
cout << "p1的年龄:" << p1.getAge() << endl;
//情人 只能设置 外部我不告诉你
p1.setLover("仓井");
}
int main(){
test01();
system("pause");
return EXIT_SUCCESS;
}
可赋予客户端访问数据的一致性。
如果成员变量不是public,客户端唯一能够访问对象的方法就是通过成员函数。如果类中所有public权限的成员都是函数,客户在访问类成员时只会默认访问函数,不需要考虑访问的成员需不需要添加(),这就省下了许多搔首弄耳的时间。
可细微划分访问控制。
使用成员函数可使得我们对变量的控制处理更加精细。如果我们让所有的成员变量为public,每个人都可以读写它。如果我们设置为private,我们可以实现“不准访问”、“只读访问”、“读写访问”,甚至你可以写出“只写访问”。