C++学习笔记
C++:
C的编译器叫gcc,C++的编译器叫g++
c++最容易发生的问题是内存泄漏问题,例如释放p所指的空间时,应该是
free(p);
p = NULL;
很多人忘记将p归零,这样下次不小心调用了p就会出现内存泄漏问题,如果要把释放内存写成函数,示例如下(两层指针)
void free_mem(int **pp){
if(pp == NULL){// 确保指针的指针不是0
return;
}
if(*pp != NULL){// 要释放的指针不为0
free(*pp);
}
*pp = NULL; // 将原指针归零
}
命名空间:
using spacename
-
iostream头文件提供了命名空间,标准命名空间是std
-
如果不加
using spacename std;
,使用cout和endl需要声明在哪个命名空间里,例如:std::cout << “hello” << std::endl
或是声明使用命名空间中的一个变量:using std::cout
-
作用域:在函数内声明就作用在函数内,在函数外声明就作用在.cpp文件全局,可以在头文件中声明,但前提是头文件中有对应的提供了命名空间的头文件
自定义命名空间:
嵌套命名空间必须引用到底部,比如spaceA中有spaceB和变量a,spaceB中又有spaceC,spaceC中有变a,要引用spaceC中的a必须写using spaceA::spaceB::spaceC::a
新增数据类型bool:
bool类型的数据大小为1字节,true就是1,bool类型可以赋值成不为0和1的数,但是c++会自动将其变成0或1(所有非0赋值都是1)
const优化:
const修饰的变量只能被匹配的指针指向
例如:const int a = 10;
此时a只能被下列1和3形式的指针指向
-
const int *point1
表示point1所指的内存不可修改,但point1本身的地址值可以修改 -
int * const point2
表示point2本身地址值不可修改,但其所指内存可以修改 -
const int* const point3
表示point3本身地址不可修改,且point3所指的内存也不可修改 -
被const修饰的变量不可以通过指针修改
即const直接跟变量名,修饰的是指针;const在(指针)类型前,修饰的是指针内存
指针与const交互
-
在c++中,const修饰的常量在编译时会被添加到符号表中,而不是为其开辟一块新的内存空间。类似于键值对,变量名与对应的值一一对应,当程序遇到常量时会直接将其替换为对应的值,即和常数是等价的,对常数取地址显然是错误的:
int* p = &1
(符号表是在一段只读内存中,不可获取其地址并修改) -
承接1.来说,
int* p = &1
也是可以编译通过的,g++会临时开辟一个空间temp,让p存放temp的地址,这个地址也可以进行修改等操作
在c++中,被const修饰的变量 一定不可修改
与define的区别
- 代码成为可执行文件需要先预处理再编译,define是在预处理时就执行,是把代码完全展开丢给编译器
枚举enum规范化:
- 创建枚举类对象并初始化时,必须使用枚举中列出的名称,即使该名称的赋值是常数,也不能用常数代替;进行运算时,枚举类变量也必须和枚举中的名称进行运算,不能用常数代替,也就是说,枚举变量必须被赋值为枚举类值
引用:
-
typedef不仅可以命名数据类型,还能将变量命名成其他名字,也就是起个别名,这个别名就叫引用,但定义方式不再用typedef
-
引用并不等于指针,定义一个指针
p
让它指向a
,虽然可以通过p操作a,但是p本身地址值是可变的,p并不是永久指向a的,而为a定义一个引用b,则隐式指针b永远指向a,就可以说b是a的“别名”,即引用,可以直接通过引用操作变量
指针和引用的主要区别就在于,所指向的位置会不会发生改变
- 引用的定义方式:
int& r = a
及表示定义一个r为a的引用,区别于指针int*
引用的特性:
- 引用没有定义,是一种关系型声明,声明它和原有某一变量有关系,因此类型和原类型保持一致,且不分配内存。与被引用的变量有相同的地址。
- 声明时必须初始化,一经声明,不可变更。
- 可对引用再次引用。多次引用的结果是某一变量具有多个别名。
- &符号前有数据类型时是引用,其他的时候都是取地址。
- 引用的大小跟指针是相等的,本质是常指针(int *const),其变量名存放在常量区
引用作参数:
- 引用不能当指针,进行传参时,传入引用作为参数,不能修改所指的变量。可以将引用当成变量来传参,传引用的地址(引用和变量本身具有相同地址)
- 引用可以作参数且具有指针同等修改能力,函数体定义如下(假设参数是引用a):
void change_value(int& r){ // int& r = a; r = 10; // a = 10 }
引用作为函数的返回值
int& getA(){
int a = 10;
return a;
}// int& temp = a;
void main(){
int main_a = 0;
main_a = getA();//main_a = temp;
}
在上面的例子中,getA()
会创建一个a的引用temp并返回,temp在执行完a = getA()
后就会销毁,本质上返回的是局部变量的指针,是危险的行为
如果把main改一下
void main(){
int &main_a = getA();
cout << "main_a = " << main_a << endl
}
输出结果就是乱码,因为执行完 int &main_a = getA();
后,temp就被销毁了,故temp的引用main_a指向的内存也不能正确读出来
可以返回static变量的引用,因为它本质还是全局变量,在堆内存,此时可以把函数当成左值,例如:
int& getA(){
static int a = 0;
return a;
}
void main(){
getA() = 10;// a = 10
}
传参方式对性能的影响:
场景:输出结构体数组中所有成员(学生类型)的信息,实现输出函数。
直接传参:
需要创建结构体形参并对传入的结构体进行值拷贝,当结构体很多时消耗大量内存
void printfS(struct student s){ // struct student s = s1
cout << s.id << s.name << endl;
}
传指针:
需要拷贝指针,拷贝量小,但需要指针操作,不熟悉指针的人(Java转c++)可能出错
void printfS1(struct student* s) { // struct student* s = &s1
cout << s->id << s->name << endl;
}
传引用:
需要拷贝引用,拷贝量小,且直观易懂
void printfS2(struct student& s){ // struct student& = s1
cout << s.id << s.name << endl;
}
指针引用:
示例如下,引用可以看作一层指针,传入引用就可以修改引用所指的内存,指针本身也可以被引用
int getmem(struct teacher*& tpp) {
struct teacher* tp = NULL;
tp = (struct teacher*)malloc(sizeof(struct teacher));
if (tp == NULL) {
return -1;
}
tp->id = 1000;
strcpy(tp->name, "zhang 6");
tpp = tp;
return 0;
}
const引用:
如果要引用const型变量,必须用const型引用来定义
int const a = 10;
int const &b = a;
三目运算符增强:
C++的三目运算符计算结果可以当左值,运算结果返回的是引用(参考java隐藏指针)
例如a<b ? a:b = 10
如果满足a < b返回的不是a的值,而是a本身(左值),并且可以被赋值为10
inline内联函数:
适用于简单多次调用的函数,过于复杂的函数(内部有循环)即使写成内联函数也可能不生效
功能是牺牲用空间效率换时间效率,代码体会很庞大
在函数前加上inline
定义声明方法:
inline void output(a,b);
inline void output(a,b){
cout << a << b << endl;
}
函数的执行中需要经历压栈和出栈、值拷贝等等,内联函数的作用类似宏函数,但是内联函数由编译器展开,能够进行语法检查和复杂传参(宏函数由预处理器处理,没有语法检查)
for(int i = 0 ; i < 1000; i++){
.....
function(a,b);
.....
}
如果function是内联函数,编译器会直接将function在调用处展开,类似宏定义,而不进行压栈和出栈
注意:内联函数在声明和定义时都需要加inline,如果定义不加,则还是普通函数
参数:
默认参数
设置默认参数,允许空参调用。
void func(int a = 666){
cout << "a=" << a << endl;
}
void main(){
func();
}
多个参数设置规则:
有默认值的参数必须在形参列表末尾
void func(int b,int a = 666){
cout << "a=" << a << endl;
}
占位参数
顾名思义,只占位置,不会在函数中使用,调用时必须填上这个位置,用于预留位置
int func(int a,int){
cout << a << endl;
}
重载:
c++支持重载
在函数名相同的前提下:
- 参数个数不同
- 参数类型不同
- 参数顺序不同
均可构成重载
注:重载不关心返回值,同名同参数列表,不同类型返回值的函数之间,不能重载
重载和参数细则:
-
需要重载时不要加默认参数,产生调用歧义就报错
例如:int func(int a,int b,int c = 1){ ....... } int func(int a, int b){ ....... }
在用
func(1,2)
调用时不能确定到底是以上哪个函数 -
需要重载时可以用占位参数,不会产生调用歧义
-
参数个数相同优先调用,通过隐式转换能匹配类型的
例如:调用fa(1)
void fa(double a) { cout << "double"; } void fa(int a) { cout << "int"; }
fa(1);
会优先调用参数为int型的fa,如果没有第二个fa,则调用第一个
函数指针:
函数指针的作用,主要是能够把函数作为参数进行传递
定义函数类型
- 定义方法:
typedef int(MY_FUNC)(int,double)
解释:
typedef [返回值类型]([自定义函数名称])([形参类型列表])
定义函数指针
定义方法(最常用):
MY_FUNC* fp = NULL;
fp = func;
fp(10,20);
.....
定义指向某函数类型的指针类型
就是把返回类型改一下,有两种写法。(*符号位置不同)
第一种:
typedef int*(MY_FUNC_P)(int,int);
第二种:
typedef int(*MY_FUNC_P)(int,int);
函数指针调用方法:
MY_FUNC_P fp = NULL;
fp = func;
fp(1,1);
定义指向函数类型的指针类型不常用,因隐藏了*符号容易忘记这是指针
不用typedef定义函数指针
示例:
int(*fp)(int,int) = NULL;
fp = func;
重载和函数指针交互
-
在给函数指针赋值时,就进行函数重载匹配
-
函数指针只会指向匹配的参数列表的函数
-
一旦匹配后,通过指针调用时就不再为重载进行匹配
swap函数:
在很多算法中,特别是排序算法,经常需要交换元素的位置,所以std::swap在实践中非常有用。定义如下:
template <class T>
void swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
使用方法为swap(a,b)
这里的T是一个模板参数,表示可以是任何类型。因此,可以使用std::swap来交换整数、浮点数、字符串、自定义对象等的值。
类和对象:
结构体就是一个类,默认访问权限是public,里面可以写函数或设置访问权限
访问权限(公私有)
-
public和private都与Java一样
-
protected与Java不同
C++:protected 仅可被子类和拥有该成员的类访问
Java:protected不仅可以被子类、拥有该成员的类访问,同一个包内的类也可以访问
-
不写访问权限修饰符,默认是private
对象作参数
-
一般传的都是对象的引用,方便的修改或调用
-
有对象做参数的函数格式
void eat(class Dog &dog);
多文件中写类
c++写类的标准是:
.h头文件 + .cpp源文件
头文件中写声明,源文件中写定义,格式如下。
Circle.h头文件:(不要忘记#pragma once
)
#pragma once
class Circle
{
private:
int r;
double c;
public:
Circle(int a);
int getR();
double getC();
};
Circle.cpp源文件:
#include "Circle.h"
int Circle::getR() {
return r;
}
double Circle::getC() {
return c;
}
Circle::Circle(int r) {
this->r = r;
}
需要创建该类的对象时,需要引入Circle头文件include "Circle.h"