C++学习笔记

cupCheng / 2023-09-05 / 原文

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

  1. iostream头文件提供了命名空间,标准命名空间是std

  2. 如果不加using spacename std;,使用cout和endl需要声明在哪个命名空间里,例如:std::cout << “hello” << std::endl
    或是声明使用命名空间中的一个变量:using std::cout

  3. 作用域:在函数内声明就作用在函数内,在函数外声明就作用在.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形式的指针指向

  1. const int *point1 表示point1所指的内存不可修改,但point1本身的地址值可以修改

  2. int * const point2 表示point2本身地址值不可修改,但其所指内存可以修改

  3. const int* const point3 表示point3本身地址不可修改,且point3所指的内存也不可修改

  4. 被const修饰的变量不可以通过指针修改

即const直接跟变量名,修饰的是指针;const在(指针)类型前,修饰的是指针内存

指针与const交互

  1. 在c++中,const修饰的常量在编译时会被添加到符号表中,而不是为其开辟一块新的内存空间。类似于键值对,变量名与对应的值一一对应,当程序遇到常量时会直接将其替换为对应的值,即和常数是等价的,对常数取地址显然是错误的:int* p = &1(符号表是在一段只读内存中,不可获取其地址并修改)

  2. 承接1.来说,int* p = &1也是可以编译通过的,g++会临时开辟一个空间temp,让p存放temp的地址,这个地址也可以进行修改等操作

在c++中,被const修饰的变量 一定不可修改

与define的区别

  1. 代码成为可执行文件需要先预处理编译define是在预处理时就执行,是把代码完全展开丢给编译器

枚举enum规范化:

  1. 创建枚举类对象并初始化时,必须使用枚举中列出的名称,即使该名称的赋值是常数,也不能用常数代替;进行运算时,枚举类变量也必须和枚举中的名称进行运算,不能用常数代替,也就是说,枚举变量必须被赋值为枚举类值

引用:

  1. typedef不仅可以命名数据类型,还能将变量命名成其他名字,也就是起个别名,这个别名就叫引用,但定义方式不再用typedef

  2. 引用并不等于指针,定义一个指针p让它指向a,虽然可以通过p操作a,但是p本身地址值是可变的,p并不是永久指向a的,而为a定义一个引用b,则隐式指针b永远指向a,就可以说b是a的“别名”,即引用,可以直接通过引用操作变量

指针和引用的主要区别就在于,所指向的位置会不会发生改变

  1. 引用的定义方式:int& r = a及表示定义一个r为a的引用,区别于指针int*

引用的特性:

  1. 引用没有定义,是一种关系型声明,声明它和原有某一变量有关系,因此类型和原类型保持一致,且不分配内存。与被引用的变量有相同的地址。
  2. 声明时必须初始化一经声明,不可变更
  3. 可对引用再次引用。多次引用的结果是某一变量具有多个别名
  4. &符号前有数据类型时是引用,其他的时候都是取地址。
  5. 引用的大小跟指针是相等的,本质是常指针(int *const),其变量名存放在常量区

引用作参数:

  1. 引用不能当指针,进行传参时,传入引用作为参数,不能修改所指的变量。可以将引用当成变量来传参,传引用的地址(引用和变量本身具有相同地址)
  2. 引用可以作参数且具有指针同等修改能力,函数体定义如下(假设参数是引用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++支持重载
在函数名相同的前提下

  1. 参数个数不同
  2. 参数类型不同
  3. 参数顺序不同

均可构成重载

注:重载不关心返回值,同名同参数列表,不同类型返回值的函数之间,不能重载

重载和参数细则:

  1. 需要重载时不要加默认参数,产生调用歧义就报错
    例如:

    int func(int a,int b,int c = 1){
     .......
    }
    
    int func(int a, int b){
     .......
    }
    

    在用func(1,2)调用时不能确定到底是以上哪个函数

  2. 需要重载时可以用占位参数,不会产生调用歧义

  3. 参数个数相同优先调用,通过隐式转换能匹配类型的
    例如:调用fa(1)

    void fa(double a) {
     cout << "double";
    }
    void fa(int a) {
     cout << "int";
    }
    

    fa(1);会优先调用参数为int型的fa,如果没有第二个fa,则调用第一个

函数指针:

函数指针的作用,主要是能够把函数作为参数进行传递

定义函数类型

  1. 定义方法:
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;

重载和函数指针交互

  1. 在给函数指针赋值时,就进行函数重载匹配

  2. 函数指针只会指向匹配的参数列表的函数

  3. 一旦匹配后,通过指针调用时就不再为重载进行匹配

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,里面可以写函数或设置访问权限

访问权限(公私有)

  1. public和private都与Java一样

  2. protected与Java不同

    C++:protected 仅可被子类拥有该成员的类访问

    Java:protected不仅可以被子类、拥有该成员的类访问,同一个包内的类也可以访问

  3. 不写访问权限修饰符,默认是private

对象作参数

  1. 一般传的都是对象的引用,方便的修改或调用

  2. 有对象做参数的函数格式

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"