为什要学习C++
C++具有多种优良的特性
- C++是由C语言扩展而来,继承了C语言高效的语法,比如可以使用指针,可以直接访问内存。
- C++具有高级语言的特性,同时C++是编译型语言,执行效率高。
- 利用C++进行编程相对于使用C语言来说更加快捷,增加代码复用性。
- 所以C++是那些有C语言基础的人员进阶理解高级语言特性最为合适的语言。
C++语言的难点
C++支持多种编程范式,语法极其丰富且紧随潮流,支持下面这些编程方式:
- 面向过程编程
- 面向对象编程
- 函数式编程
- 泛型编程
我们需要掌握C++中的重点内容
- 堆的使用、函数重载、默认参数、引用
- 类的定义与使用(权限、成员、构造析构)
- 继承(权限、多继承)
我们需要掌握C++语言的重要特性
- 类型转换
- 静态、友元运算符重载
- C++对象的内存模型
- 以长处理
define、const、enum、inline总结
define
define是使用非常广泛的预处理命令
无参宏
- 定义一个常量的值,便于修改
- 定义一组属性,增强可读性
有参宏
- 用于经常被调用的短小代码,提高程序的运行效率
尽管define有以上优点,但是他还有一些致命的缺点
- 纯替换,有参宏中容易产生误解
- 没有类型检查,调用中容易出错
- 没有指出这些属性值是有关联的
示例:
#define MAX(a,b) a>b?a:b
int main()
{
int n1 = 1,n2 = 0;
int n3 = MAX(n1,n2);
//在有参宏中使用自增运算符的时候,就导致了逻辑上的错误
}
const
- const是一个神奇的关键字,神奇之处在于告诉编译器这个变量不允许改变,编译器保证其不会改变。(他仍然还是一个常量,但是它再也不能被赋值了)
- const定义的常量是由类型的,在编译的时候会进行类型安全检查,这是const优于define的地方,所以我们推荐使用const代替define定义常量。
const处理定义变量之外还有很多的用法:
- 函数参数声明为const
- 返回值声明为const
- 类成员函数声明为const
const是一个常量修饰符,在C++中常量的一些使用规则:
- 常量必须被初始化
- 在使用常量的时候,编译器会直接将常量名用它的初始值来进行替换(和无参宏一样,在源码中使用宏的名字,经过预处理之后会被替换成宏的内容)
const使用场合
修饰指针常量指针
:将指针指向的内容修饰为常量.(指针可以被指向被const修饰过的变量,这就意味着我们不能通过指针来修改引用的值 )
示例:
char szBuff[100] = {"hello"}; //这个会直接存到栈区
const char* pStr = "Hello"; //这个存在常量区
szBuff[0] = 'A';//可以修改
//p就是常量指针
//1.指向的内容被修饰为常量,因为不能通过p去修改内存。
//2.p自身是可以被修改的
//3.const只是在语法层面上限制一个指针不能修改它指向的内容
//,至于指针指向的内容能不能被修改是不受const的改变。
const char* p; //可以不初始化
p = szBuff;
const char * p1 = "hahaha";
szBuff[0] = 'B';//仍然可以被修改
p[0] = 'A';//语法错误,p不可以被修改,不能通过p修改
p1[0] = 'A';//语法错误
//只要一开始的时候,数据不是在常量数据去都可以被修改,常量数据区不可被修改(存的是字符串)
常量指针总结:
指针可以修改为指向不同的常量指针
可以修改为指向不同的变量
可以通过解引用来读取指针指向的数据
不可以通过解引用修改指针指向的数据
指针常量
:将指针自身修饰为常量
示例:
char szBuff[100] = {"hello"}
//定义一个指针常量p,指针常量必须被初始化
char* const p = szBuff;
//1.指针不能再指向其他内存
p = "hello";//错误,p只能指向szBuff
p[0] = 'A';//在语法上是通过的
printf("%s",szBuff);//这个最后输出的内容是Aello
//char * const p2 = "hahaha";
//p2[0] = 'A';
//在语法上是通过的,在运行的时候就报错,因为p2 存的是一个常量字符串的地址,常量字符串不能修改
指针自身不可以被修改,指针指向的值可以被修改
定义指针的时候,
*
在const左边,指针是一个常量指针
定义指针的时候,*
在const右边,指针是一个指针常量
修饰引用
int n = 100;
// 定义一个引用.
int& rNum = n;
rNum = 10; // 实际就是在修改变量n
// 常量引用.
// 不能rNum2去修改了.
const int& rNum2 = n;
rNum2 = 20; // 报语法错误
修饰成员函数
实际修饰的是this指针,也就是将this指针修饰成常量指针。
Class MyClass
{
int m_nNum;
public:
void fun()
{
m_nNum = 100;
}
}
void fun(const MyClass* pObj)
{
pObj->fun();
//语法错误
//原因分析:
//1. pObj是一个常量指针
//2. 成员函数有条件修改自身的成员变量,这相当于通过指针调用了成员函数,成员函数在内部修改了成员,实际上就相当于间接通过指针修改了指针指向的内存,这个是常量指针所不允许的。
}
修改版:
class MyClass
{
int m_nNum;
public:
//const修饰的是this指针
//this就相当于const MyClass* this;
void fun() const
{
this->m_nNum = 100;//此处会报语法错误
//报错:错误1.表达式必须是可修改的左值,错误2.由于正在通过常量对象访问m_nNum,因此挖法对其进行修改
}
}
void fun(const MyClass* pObj)
{
pObj->fun();//此处不会报错
}
总结:
在成员函数后加上const
, 成员函数就是一个常量成员函数, 在这样的函数内部不能修改自身的成员变量, 也不能调用其它的非常量成员函数。
通过常量对象指针(const MyClass* pObj
), 只能调用常量成员函数.
引用
性质
引用类型不占用内存空间,其内存空间来自被引用的变量
- 定义的时候必须初始化
- 不能再引用其他变量
- 修改自身的时候,被引用的变量被会被修改,被应用的变量修改了,引用类型的变量也会被修改,因为他们使用的是同一块内存空间。
使用场合
传参
void swap(int& l,int& r)
{
int t = l;
l = r;
r = t;
}
int main()
{
int n1 = 10,n2 = 20;
swap(n1,n2);//执行之后,n1n2交换
}
传递函数返回值
int& fun(int& n)
{
return n;
}
int main()
{
int nNum = 100;
fun(nNum) = 200; //修改了nNum
int n2 = fun(nNum);
n2 = 10; //nNum没有被修改,因为n2不是引用类型的变量,它拥有自己的内存空间,所以修改了n2不会影响nNum
int & rNum = fun(nNum);
rNum = 10; //修改nNum的内存空间,因为rNum是一个引用
}
类型转换const_cast
:常量类型转换-去除常量类型。
void fun(const char* pStr)
{
pStr[0] = 'A'; //语法报错
//1.C语言风格
char * p = (char*)pStr;
p[0] = 'A'; //不会报错
//2.C++风格
char *p2 = const_cast<char*>(pStr);//类似于(char*)pStr
}
int main()
{
char buff[100];
fun(buff);
}
static_cast
:静态类型转换-编译器认可的,例如char
转为int
void fun(int n)
{
}
int main()
{
//编译器3.14从double类型隐式转换成int 类型再传参
//C语言风格
fun(3.14);//语法可以通过,但是会报一个警告:精度丢失
fun((int)3.14);//强制转换为和形参一样的类型
//2. C++
fun(static_cast<int>(3.14))
}
reinterpret_cast
:强制类型转换-编译器不认可的,例如int*
转int
用于编译器无法进行隐式转换时的类型转换
int main()
{
int * p = 0x300000; //报错:语法错误,类型不匹配
//C语言风格
p = (int*)0x300000; //将我们赋的值强转为和p一样的类型
//C++风格
p = reinterpret_cast<int*>(0x300000);
}
dynamic_cast
:动态类型转换,用于将父类和子类的指针或应用进行转换(继承的时候会用到,能够将其类转换为派生类)
class Base
{
};
class MyClass:public Base
{
int nNum[100];
};
class B:public Base
{
int m_nNum;
};
int main()
{
Base* obj = new B;
MyClass* pObj = obj; //语法错误
//Base* 类型的值不能用于初始化MyClass* 类型的实体
//可以在语法上通过。
//但在运行的时候会出现严重问题,因为obj本质上
//并非是MyClass的类型,而是B类型
pObj = (MyClass*)obj;
//dynamic_cast可以检测是否能够转换
//如果不能就返回nullptr
pObj = dynamic_cast<MyClass*>(obj);
}
enum
经常遇到这样的问题:为某一些属性定义一组可选的值,例如:
#define UP 0
#define DOWN 0
#define LEFT 0
#define RIGHT 0
缺点是从语法上,没有指出这些值是有关联的,enum就是它的一种替代方法,不但可以定义整数常量,还可以分组。
enum DIR
{
UP,
DOWN,
LEFT,
RIGHT
};
使用枚举在函数传参的时候还可以进行参数检查。
枚举常量代表该枚举类型的变量可能取的值,编译系统为每个枚举常量指定一个整数值,默认状态下,这个整数就是所列举元素的序号,序号从0开始。 可以在定义枚举类型时为部分或全部枚举常量指定整数值,在指定值之前的枚举常量仍按默认方式取值,而指定值之后的枚举常量按依次加1的原则取值。 各枚举常量的值可以重复
inline
inline能够实现和有参宏一样的替换功能,不同的是inline会检查参数而有参宏不会检查参数类型。
规则:
- inline关键字必须和函数体定义放在一起才可以实现内联,仅仅将inline放在函数声明之前不起任何作用。
- 在函数名前加上inline关键字这个函数才可能称为内敛函数。
- inline指令就像register,他只是对编译器的一种提示,而不是命令。也就是说,只要编译器愿意,他就可以随意地忽略掉你的指令,事实上编译器也会这么做。例如,大多数编译器拒绝内敛“复杂”的函数(例如循环和递归的函数)
称为内敛函数的标准:
- 函数体必须小(代码行数少)
- 函数内部不能有复杂的逻辑(switch,递归函数、各种嵌套的循环或选择结构都是不行的)
- 用法和函数一样
- 功能和有参宏差不多(直接将函数调用替换为函数体的代码)
inline int max(int a,int b)
{
return a>b?a:b;
}
int main()
{
int n1 = 1,n2 = 2;
//正常方式调用函数
//但是表意之后,这里的函数会替换成上面的函数体
//就类似于有参宏在预编译之后会被替换一样
//但是如果实参是表达式的时候,会先计算出表达式的结果,然后再进行替换
}
内联函数你调用一万次,他就重新定义一万次,也算是内联的一个缺点,但是内联一般较小,影响不大。
总结:
通过上面的介绍,在C++中,建议尽量少的使用#define。
1.使用const与enum代替无参宏的使用
2.使用inline代替有参宏的使用