C++:查漏补缺笔记

数组

一维数组

初始化数组

  • 第一种:数组定义空间大小但不手动赋值(默认初始化)

    • 1.1
      不初始化,只定义,后面不赋值(没有意义
      这种直接开辟空间的是没有意义的,如果这样初始化后,必须记得后面要进行赋值操作,下面的代码打印出来就是一个没有意义的数值。
    	int nums[5]; 
    	int len = sizeof(nums) / sizeof(int);
    	for (int i = 0; i < len; i++) {
    		cout << nums[i];
    	}
    
    • 1.2
      另一种是定义,并 默认初始化,给一个大括号就是默认初始化 ,不同类型会产生不同的默认值。
      数值类型(int,float,double…):默认值是0
      字符类型(char):默认值是空字符
      长度是根据你给出的数组大小决定的,
      不会因为没有赋值而长度变成0。
    	int nums[5]={}; 
    	int len = sizeof(nums) / sizeof(int);
    	for (int i = 0; i < len; i++) {
    		cout << nums[i];
    	}
    	cout << len << endl; 
    	//长度是5,不会因为没有赋值长度变成0
    
  • 第二种:数组定义空间大小并赋值

    • 定义数组并给初始化的空间全部手动赋值
      直接给数组的空间赋值,并且每一个数值的类型必须都是一样。(五个int空间就赋值5个数值)
    	int nums[5]= { 1,2,3,4,5 }; 
    	int len = sizeof(nums) / sizeof(int);
    	for (int i = 0; i < len; i++) {
    		cout << nums[i];
    	}
    
    • 定义数组并给初始化的空间部分手动赋值,后面的默认初始化
      数组是5个空间,但是只初始化了4个数值,那么剩下的就会用0默认赋值。
      数值类型(int,float,double…):默认值是0
      字符类型(char):默认值是空字符
    	int nums[5]= { 1,2,3,4}; 
    	int len = sizeof(nums) / sizeof(int);
    	for (int i = 0; i < len; i++) {
    		cout << nums[i];
    	}
    
  • 第三种:数组不定义空间大小,但需手动赋值。
    中括号内不显示的定义多少个空间,那么就必须在花括号内赋值,赋值后会自动帮你计算出来你赋值了多少个数值,然后你的数组空间大小就是多少。
    不可以即不显示写出空间的大小也不进行花括号赋值初始化,这样一般的代码编辑器不用等你编译后都会显示的告诉你代码错误
    错误示范→:nums[]={}

    	int nums[]= { 1,2,3,4,5}; 
    	int len = sizeof(nums) / sizeof(int);
    	for (int i = 0; i < len; i++) {
    		cout << nums[i];
    	}
    

数组名

  • 数组名的含义
    • 直接打印数组名即数组的地址
    • &数组名,即数组地址
    • 数组第一个空间的地址(首地址,&数组名[0]
	//以下输出的三个都是地址,并且都是同一个地址
	cout << nums << endl; 
	cout << &nums << endl;
	cout << &nums[0] << endl;
  • 取出的地址进行sizeof量长度大小的话
    • x64(64位编译环境下)
      地址长度为8字节
    • x86(32位编译环境下)
      地址长度为4字节

二维数组

二维数组其实本质还是一维数组,只不过可以用二维坐标的方式取出数据而已,其实你还是可以只使用一个行就能取出所有元素。
比如nums[0][0~end]可以只使用第0行就能取出后面所有的元素,因为二维数组的元素还是一串连续的地址空间,因此即使你使用超过该行的列数,那么他二维数组会把你该列模该行的列数的结果就是下一行的元素列下标(当然是不超过下标的情况)。
二维数组定义的四种方式

  • 1:数据类型 数组名 [ 行数 ][ 列数 ];
    初始化,同一维数组一样,如果后面不进行赋值的话就没有意义。

  • 2:数据类型 数组名 [ 行数 ][ 列数 ] = {{数据1,数据2 }, {数据3,数据4}};
    初始化的空间多少就赋值多少数据。

    	int nums2[2][3] = { {1,2,4},{5,6} };
    

    这种初始化方式照样很随意,只要你不超过一行规定的列数,所有元素加起来不超过总空间即可,因为二维数组本质就是一维数组。
    同理:在本例子中,第二行的5,6中明显不符合三列的规定,但是在数组中会默认赋值为0,同一维数组一样
    (都说了吧,二维本质就是一维)

  • 3: 数据类型 数组名[行数][列数]={数据1,数据2,数据3,数据4};
    这里是需要你赋值的数据不要超过二维数组的总空间即可
    比如:2行3列,你用这种赋值方式只需要你所有数据个数在2×3=6个内即可。

    int nums2[2][3] = { 1,4,5,6 };
    

    这种和给每行单独赋值的区别就是:在该方法中,我们不能让计算机给我们的某一行默认赋值0,比如本方法中,他会自动的给145分在同一行,然后6作为第二行,然后后面两列没有赋值的就会自动的填充0。但是我们在用花括号给每一行进行赋值的时候可以手动的控制我们应该赋值的元素,不赋值的就自动让他默认值即可。(各个有各个的好处,看具体情况具体分析)

  • sizeof(nums[0]):这是一行的数据大小

  • sizeof(nums[0][0]):这是一个元素的大小

  • nums[0]这是一行中的首地址,相当于一维数组中的首地址

  • nums这是第一行第一列的地址,也是整个二维数组的地址

总结:二维数组和一维数组一样,二维只是多了一个行的定义,在二维中可以表现的跟一维数组一样,只用一行就可以表示二维内所有的元素(只不过是在改行内作为主位置,相对于其他元素的位置作为列下标)
好比下面的例子:甚至可以用 负号 作为列下标
int nums2[2][3] = { 1,2,5,6,1 };
cout << nums2[1][-1] << endl;

函数

函数声明

  • 函数声明可以有多次,但是函数定义只能有一次(可以选择重载函数)
    函数声明可以单出一个头文件,前提是必须在main函数之前声明,在函数定义之前声明。

函数的分文件

  • 头文件
    用来存放函数的声明和其他。
  • 函数定义文件,也就是CPP文件
    用来存放对于头文件中的函数声明的函数定义。
  • 主文件CPP
    这个就是主要程序的文件了。
  • 注意:不同文件之间相互引用才能使用彼此的代码,比如函数定义里面要使用头文件里面的东西,直接使用include即可引进来(自己编写的文件要用双引号引,不能用尖括号)
  • 主程序中一般引用的是头文件,然后定义函数文件里面也必须要引用头文件。
    解释:我们主程序引用头文件就行是因为我们在一个项目中,因此编译的时候可以通过函数声明找到函数定义的文件然后我们引用函数声明的文件即可。

函数重载

  • 函数重载的条件
    • 函数名必须相同
    • 函数的参数个数或者参数类型不同
      首先这个参数个数必须是没有默认参数的才算一个参数
      下面这两个出现二义性了,就不能构成函数重载了。
      void a(int a,int b = 10);
      void a(int a);
      
    • 函数使用引用类型进行函数重载也是可以的。
      需要注意的就是我们的引用类型本质是:int* const,因此传参的时候只需要正常传参就行,传入一个普通变量,该函数就会将你的变量转为引用类型到函数内部进行操作。
      因为函数参数就是一个赋值的过程,函数参数 = 调用函数的实参,那么在引用作为参数的时候就直接变成了:int& a = 实参,所以函数调用的过程就是一个参数赋值的过程,然后将你参数的类型转到函数内部进行使用。
      • 还有一个注意事项:const int& a,双重const的时候该变量类型本质就是:const int * const a,所以我们在调用的时候必须要传递一个常量作为参数,所以我们可以直接传define过的常量变量或者直接传递一个数字,数字就是一个常量,变量才不是常量。数值10,。。。这些都是常量,变量则是有变量名和显示指出变量类型才为变量。
  • 返回值不同不能完成重载,因为我们的重载条件仅仅是函数名相同,参数类型和个数不同即可。函数返回值不同不会影响你的函数重载,换句话说不能使用函数返回值不同来完成函数重载。

指针

指针占的内存

  • 在x86,即32位操作系统编译下,一个指针变量占:4位
  • 在x64,即64位操作系统编译下,一个指针变量占:8位
    无论是什么类型的指针都一样,因为存的指针都是进制数。

空指针

空指针是用于防止野指针的出现

  • 空指针不是野指针,空指针用NULL赋值之后是指向0的,就是防止野指针出现。
    • 控制指针不能访问,除非你给他赋值地址了,当然你赋值了就不叫空指针了都。(因此:当他还是一个空指针的时候,空指针是不能进行访问赋值的)

野指针

  • 野指针就是你的指针用完之后没有将置为空指针,那么这个指针的指向是不明确的,或者说你这个地址用完了已经释放了,但是你的指针还在指向这个地址位置,这就是没有权限的访问,越界访问就是野指针。在编写代码中不要出现空指针现象。

const

常量的意思是,被修饰的那个东西不能修改。

  • 常量指针
const int * p

常量指针,直接音译过代码就是const int *,常量const,指针int *,
然后的话常量指针修饰的是类型,所以指针指向的数值不能修改,但是我们的指针的指向的地址是可以修改的。
解释:常量的指针嘛,那你指针的地址指向的东西就是常量,即地址对应的数值不可以修改,但是 地址可以修改。

  • 指针常量
int* const p

指针常量,直接音译过代码就是int *指针,const常量,
然后指针常量说的就是指针的常量,那很明显了我们的指针地址不能修改,也就是说我们指针指向不能修改但是数值是可以修改的。
解释:指针的常量,中文意思已经很明白了,就是指针常量,那么就是指针指向的地址是常量,常量就是不能修改的意思。

  • 双重const修饰
    意思就是既修饰指针变量又修饰指针指向的值,就是上了双重锁,一旦指定一个值,地址不能变,值也不能变。
const int * const p

总结:其实字面意思和代码的书写顺序一样
常量+指针 = const int* : 常量指针
指针+常量 = int* const : 指针常量
如何区别:其实只要记住中文意思即可
常量指针:常量的指针,那就是说我的这个指针指向的值是一个常量,那就是指针指向的值是不能修改的,但是这个地址的值可以随便修改。
指针常量:指针的常量,那就说我们的这个指针变量不能随意修改,那么我们指针变量存的东西就是地址值,那么换句话说就是我们的指针常量就是不允许修改指针地址,但是地址指向的值可以随意修改。
双重const:很明显了 ,就是哪边都不能进行修改操作,直接上了两把锁。

结构体指针

在结构体中使用指针访问内部变量的时候使用->箭头。

指针++

指针++这个就是说对于一个数组指针来说,可以使用指针++的形式进行移位访问。相当于用自增下标访问所有元素。

int nums[5] = {1,2,3,4,5};
int* p = nums;
for(int i = 0; i < 5; i++){
	cout << *p++ << endl;
}

指针与函数

地址传递

经典的通过指针地址交换值函数

void swap(int* num1, int* num2){
	int temp;
	temp = *num1;
	*num1 = *num2;
	num2* = temp;
}
//调用
int a = 1;
int b = 2;
swap(&a,&b);

C++引用

基本语法

int a = 10;
int& b = a; //这个就是引用类型
  • 引用类型的本质其实就是: int * const p
    就是指针常量,指针指向的地址不等你修改,值可以修改就是引用的本质。
    • 引用必须初始化
    • 引用不可以改变(值可以改变)
      验证:
int a = 10;
int& b = a; 
int c = 1;
int& d = c;
//开始验证
//int &e;这样写是错误的
d = b;//记住这个不是改变引用,而是把b的值给了引用d

在尝试改变引用中发现是行不通的,就是说我们完成不了这件事,也就验证了这件事是不可行的,C++开发者已经把引用封装好了,所以刚刚也说引用就是指针常量。
因此记住引用就是封装好的指针常量。

  • 引用也是一个类型,因此我们可以用它作为函数参数传递一个实参
    调用参数的时候可以很方便,在函数中使用引用过来的参数也很方便。
    引用就是给变量起别名,因此调用的时候是直接使用变量的名字,然后函数中直接用&变量名的形式把调用函数的变量名接过来(当真是妙哉妙哉!!)
void swap(int& a,int& b){
	int temp;
	temp = a;
	a = b;
	b = a;
}
//函数调用
int a = 10;
int b = 20;
swap(a,b);
//我们可以很方便的在传参过程中就像传递形参一样传递实参进函数里面。

  • 常量引用
    说白了就是双重buff的const,因为引用就是指针常量,
    所以常量引用就是:const int& 变量名别名 = 变量名
    const int&本质就是:const int* const

面向对象易错点

  • this指针使用->箭头访问成员(学过Java的千万别记混了)

  • 在类中又常函数一说,在该函数中只读不可修改任何一个成员函数,
    除非成员函数前有一个关键字mutable

  • 属性名:建议都用m_大驼峰书写方式变量名(每个单词字母大写)

  • 函数名:用大驼峰变量名(每个单词第一个字母大写)

  • 同类之间的访问权限都是public,这一点在面向对象语言中都如出一辙。
    (即:同类但是不同对象之间,只要是在类的内部就可以访问该对象的私有属性。意思是在类中传入同一个类的参数对象,可以在类中直接访问该参数对象的私有属性

  • 在函数体内部想要返回一个实例化的对象必须要使用new关键字分配出来的空间才可以在执行完函数之后不会将该空间销毁。术话:只有使用new(malloc)在堆区分配出来的空间才是手动开辟手动销毁的。否则,不使用new(malloc)关键字,直接比如:Person p;这样进行实例化的对象返回之后就会将其局部变量销毁掉,因为出了new(malloc)是在堆区申请的空间,剩下的实例化方法在函数体内都是在栈上的空间地址,执行完函数就会将其销毁掉。

  • 拷贝构造与重载等号运算符不一样,拷贝构造是发生在实例化的时候,即空对象复制已存在的对象值而等号是发生在等号两边的对象都存在的时候进行=赋值虽然本质都是复制,并且都是默认浅拷贝,所以我们要在operator=重载运算符中进行对指针的深拷贝操作,操作与拷贝构造函数一样的思想,就是重新开辟一个新的堆区空间
    总结:拷贝构造与重载等号运算符不一样的地方就是一个是构造函数用来空对象=已存在的,而等号则是两个已存在的对象之间进行赋值操作。

  • 一旦一个类中有指针类型,就必须要进行拷贝构造和析构函数书写,对地址空间的进一步控制,防止内存泄漏。

  • 一旦写了拷贝构造函数就要注意是否出现对象运算符等号双方已存在的情况,并且类中存在指针类型属性的时候一定要写等号重载运算符,否则会造成内存重复释放
    因此重载等号运算符的时候一定要注意该事项,且编译不报错,否则很难找出来原因。

  • 等号运算符重载函数与拷贝函数不一样。

    • 等号运算符重载:等号两边的对象必须存在才会发生类中的等号属性进行值拷贝。
    • 拷贝构造函数:在对象实例化的时候才会发生,即:左值为空对象,等号右边为已存在的对象。将右边对象使用拷贝函数复制到空对象中。
  • 子类的静态成员一旦与父类的任何一个成员发生同名的时候,子类访问不到父类的成员,不管是否发生函数名相同参数不同看上去可以通过参数发生重载情况能够通过子类访问到父类的时候,其实子类是完全隐藏了父类的同名的成员,所以我们的子类希望访问父类同名成员的时候必须要通过父类的类名::进行访问。

  • 重写与重载的区别

    • 重写都是发生在父类与子类之间,子类重写父类的函数,且重写满足条件必须是返回值函数名参数个数类型顺序必须一模一样才算是发生函数重写
    • 重载能够发生的条件就是相反的,函数名一样即可,但是是通过参数不同来发生函数重载,即参数个数不同、参数类型不同、参数类型顺序不同也可以发生重载(但是重载不可以用返回值来发生重载)
    • 虚函数必须有实现体
    • 纯虚函数没有实现体
    • 纯虚析构必须要有实现体,但是必须类内声明类外实现。(即类内 = 0,类外使用作用域进行实现函数体)
      否则:编译不报错,运行报错。
  • 函数参数中可以写参数默认值,参数默认值只可以写在参数末尾,比如:fun(int a, int b = 0, int c = 0);,默认只可以写在参数末尾,不可以写在非末尾。

  • 假如构造函数中所有参数都有默认值,那么这个构造函数可以看成一个无参构造函数,即初始化的时候可以不带任何参数。(当然这时候编译器的默认无参构造已经不存在了,走的是自己写的构造)

  • 模板类 的函数声明注意事项:类内的所有函数的默认参数只可以在类内声明的时候写上,类外定义的时候不可以写默认参数,否则直接报错。

  • 模板类 在自己的cpp文件中跟普通类一样包含自己的.h文件声明,!!!但是在被main文件调用的时候不允许包含.h文件,想要调用模板类,在main中只能包含类模板的cpp文件

    • 总结来说就是:写模板类希望分文件写的时候建议在main函数文件中包含的是类模板的.cpp文件

零散知识点

  • cout修改输出数字个数:cout输出数字的时候可以通过cout.precision(数字个数)修改输出位数,比如cout.precision(3)就代表只有三个数字输出而且会做一个四舍五入,比如3.146输出3.15,这个意思不是保留小数后三位,而是加上个位数上的保留几位数字的意思。

    cout.precision(数字个数);
    cout<<num<<endl;
    
  • cout修改输出小数后面的精度:cout输出数字的时候可以通过cout.precision(保留小数个数);cout.flags(cout.fixed);
    也称作:定法。

    cout.precision(保留小数个数);
    cout.flags(cout.fixed);
    cout<<num<<endl;
    
  • 当我们不希望cout继续用这些设置几位小数的时候,
    即希望cout取消定点法输出的时候用↓:

    cout.unsetf(cout.fixed);
    
    • 总结上cout修改输出位数:cout.precision是设置数字位数,如果单单只是设置了cout.precision的个数就代表的是从整数位数起保留几个数,比如cout.precision(4),即保留4位数字,那输出double n = 123.45678的时候只会输出123.4,如果是设置了保留几位数后希望我们保留的这个几位数是保留小数后的数字那就需要设置cout.flags(cout.fixed);,那么当我们设置完cout.precision(4)后再写cout.flags(cout.fixed);的输出就是123.4567,即这个4的参数变成了保留小数后面呢4位
  • Long类型:建议在数字后面加大写的L,如123L
    LongLong类型:建议在数字后面加大写的LL,如123LL
    float类型:希望一个常量小数是float的话,一定要在小数后面加f
    double:希望一个常量小数是double的话,后面不加f的都默认当成double

  • 在C++中的进制如何表示(即cout输出什么格式的时候默认会当成哪个进制)
    16进制:0x为前缀
    8进制:0为前缀(没听错,8进制就是0为前缀,比如:011 = 十进制的9)
    所以:
    当我们cout输出0x某某数字的时候这个就会当成16进制,而我们cout以0开头的数字就会当成八进制数字输出,输出的时候会自动帮我们把你该进制的数字转化成十进制输出来。

日后继续补充细节…


posted @ 2023-07-06 00:46  竹等寒  阅读(12)  评论(0编辑  收藏  举报  来源