C++中你可能会忽略掉或者搞错的重要知识

一、顶层const和底层const

修饰的是带有指针类型的变量。

这里我们先区分下要讲的概念:

int a = 10;
int *p = &a;

指针本身是指p(值为&a),指针指向的对象是指*p(值为a)

你可以这样理解顶层:就是最顶端,最原始的东西,一个变量有一个值和地址,值是通过地址得到的,所以,地址在顶层,而const是说常量,那么合起来,顶层const就是地址是一个常量,那么底层const就是说值是一个常量

顶层const:

int a = 10;
int * const p = &a;
  • 修饰的是指针本身(也就是p),表示地址不可以修改。也就是说不能修改p的指向(p的值始终只能是&a),但是可以修改p指向对象的值,也就是修改*p(也就是a)的值。
// 接上
int b = 15;
p = &b; // 这里是错误的,p是顶层const,不能修改 
*p = b; // 这里是正确的,修改的是*p的值,也就是指针指向的对象的值。

底层const:

int a = 10;
const int* p = &a; // int const* p = &a; 都是正确的。

修饰的是值,表示指针指向的对象的值不可以修改。*p(也就是a)的值不能修改。

区分方法:

最简单的区分方法是看const关键字的位置:

int * const p

放在*右边就是修饰p,修饰的是地址,就是顶层const

int const *pconst int *p

放在*左边,表示修饰的是*p,修饰的是值,就是底层const

二、指针和引用

引用:并非对象,是对另一个已经存在的对象起的另外一个名字。
指针:是一个对象,指向一种类型的复合类型。

这里最大的区别在于,引用不是对象,而指针是对象,怎么去理解呢?就是指针可以获取到指针本身的地址,例如int *a; &a是指针本身的地址值,而a是指针所指的对象的地址。而引用不一样,它本身没有地址,它的地址为引用的对象的地址。

这里会有这样几个现象:

①指向指针的指针:

int *a; // a是一个对象(指针本身是对象)
int* *b = &a; // 正确的,b是指向一个指针(int*)的指针

对于指针来说,指针本身是一个对象,是有地址的。所以可以把指针本身看作一个对象,这时候用其他的指针指向这个对象,这是没有问题的,这里无论套多少层都是可以看成指向指针的指针。

②指向引用的指针(不存在的,是错误的,不要以为是对的!

int a = 10;
int &b = a;
int &*p = &b; // 错误!相当于 int &(*p) = &b;

我们知道引用只是给其他变量取的一个别名,那么本身是不应该存在有地址一说的,*p是个指针,本身就具备地址,就与引用产生了矛盾,所以这里是不成立的。

三、引用的一些用法:

①允许常量引用绑定非常量的对象,反之则错误

int a = 10;
const int &b = a; // 正确

const int &c = 10;
int d = c; // 错误

我们进行类型转换的时候,我们可以将高精度、高容量向低精度、低容量直接转化。所以这里只能将非常量直接转为常量

四、char类型

①char类型的定义:

  先说下char,这是c风格字符串,是一种约定俗成的写法,字符串及字符数组都要以空字符结尾(‘\0’)。也就是说我们定义char* str = “123”;这个字符串后面会存在一个空字符。(注意string类型不一样,string类型没有结尾的’/0’)

char* str = "123";
cout << sizeof(str) << endl; // 这里输出的值为4,因为str的内容实际为:{'1' '2' '3' '\0'}

②char类型的输出:

char a = 's';
cout << a << endl; // 输出 s
cout << &a <<endl; // 错误,出现不确定值

单字符输出的情况,是直接输出字面值。如果采用地址输出的话,就会变成乱码,因为c风格字符串默认以'/0'结尾,它会不断读取直到出现'\0'为止,所以基本输出都是乱码,不排除运气好,正好后面出现了结束符,这样可能没有乱码。

char *s = "hello"; // 会有警告,由于低容量转化为高容量,正确无警告:const char* s = "hello";
char s2[] = "hello";

cout << *s << " " << *s2 << endl; // 输出 h h
cout << s << " " << s2 << endl; // 输出hello hello

字符串输出,字符串的变量名表示字符串的首地址,输出这个首地址,就可以输出整个字符串了(字符串在定义的时候默认最后一位是'/0'字符)。

如果加上解引用符号*的话,代表的是单个值,也就是字符串首地址对应的字面值

五、char *s 和 char s[]的区别

char* s = "hello";
char s2[] = "hello";

先说他们的区别吧,s是指针,它的特性取决于它指向的对象,比如这里的话,它指向了一个字符串常量,所以s[n]是不可以修改的,但是s的指向是可以随便修改的(所以可以进行自增减等运算),下面的操作是正确的!

while(s!= '\0')
{
	cout << *s << endl; // 注意s指向的是常量,所以不能改变它的值
	s++;
}

s2的话,是一个分配了地址的字符数组,它里面的值只跟他自己定义有关,我们定义的是非常量字符数组的话,s2[n]的值是可以修改的。但是s2是分配了具体的地址,所以s2的指向是不能修改的(所以不能进行指针的一些计算)。

while(*s2 != '\0')
{
	cout << *s2;
	s2++; // 错误,这里的s2不是指针,它是一个实际的地址,这个地址值是不能修改的
}

for(int i = 0; i < sizeof(s2) - 1; i++)
{
	cout << s2[i]; // 正确,并可以改变它的值
}

注意:

我们要知道,指针是一个对象,指针有自己的地址,它指向的类型的地址,是作为值保存在这个地址上,可以见下图:(用于解释上述内容,不具准确性)

在这里插入图片描述
对于第一种情况,指针并不是直接存储"hello"这个字面值,而是保存它字面值存放的地址,由于"hello"是一个字面值,是一个常量值,是直接分配在常量池的,我们s只是指向它,由于这个值是常量值,所以它的值没法修改,而且定义的时候还会有警告,如果我们加上底层const修饰,警告会消除。

六、int (*ptr)[10] 和 int* ptr[10]的区别

① 这里先讲下数组的一些只是,数组不允许拷贝和赋值(prime c++上是这样写的,但是我觉得应该是数组名不允许拷贝和赋值)

int a[] = {0,1,2,3};
int a2[] = a; // 错误,不允许拷贝
a2 = a; // 错误,不允许赋值

② 其次不存在引用的数组。

int& a[10] = xxx; //错误,引用不是对象,没法分配内存,所以这样的数组是不存在的。

③ &数组名:表示指向整个数组的指针。
数组名:表示数组第一个元素的地址。

下面就是正式的内容了:

int (*ptr)[10]:

这里的*修饰的是ptr,说明ptr是个指针,它是一个指向有10个整型变量的数组的指针。

int a[10] = {0,1,2,3,4,5,6,7,8,9};
int (*ptr)[10] = &a; // 这里只能使用&a,如果使用a就会出错,他会告诉你:不能将int *类型传递给int (*)[4]这种类型

int ptr[10];*
这里的ptr是一个数组的首地址,该数组有10个对象,每个对象的类型是int*,就是保存了10个整型指针的数组。

int a = 10;
int* b = &a;
int* ptr[10] = {b,b,b,b,b,b,b,b,b,b}; // 这里面保存的全是 int* 指针

七、函数指针

函数指针指向函数而非对象,和其他指针一样,函数指针指向某种特定类型。函数的类型由它的返回值和形参共同决定,与函数名无关。

bool* pf(const string&,const string&);
bool (*pf)(const string&,const string&);

比较上面两种声明方式,第①种并不是函数指针,因为*修饰的对象变成了返回值(优先级的原因),这里就变成了pf是一个函数了,返回值是bool*类型。

第②种方式为正确的函数指针的声明,*修饰的是pf,表示pf是一个指针,指向一个返回值为bool,参数为(const string&,const string&)的函数。

由于函数的类型是由返回值和形参决定的,所以函数指针代表的为一类函数,具体要使用哪个函数,我们还得给他赋值。

int testFunc(int a, char c, float f)
{
    cout << a ;
    cout << c;
    cout << f << endl;

    return a;
}

int (*pf)(int,char,float);

int main()
{
	int a = 0;
	char c = 'k';
	float f = 1.0;

	pf = testFunc; // 这里也可以写 pf = &testFunc;
	int sum = pf(a,c,f);
	cout << sum << endl;
}

结果截图:
在这里插入图片描述

八、函数指针形参

函数指针作为参数传递:

void func(bool pf(const string&,const string&,...))
void func(bool (*pf)(const string&,const string&,...))

int testFunc(int a, char c, float f)
{
    cout << a ;
    cout << c;
    cout << f << endl;

    return a;
}

void func(int a,char c,float f,int myFunc(int,char,float)) // 或者写成 func(int a,char c,float f,int (*myFunc)(int,char,float))
{
	int sum = myFunc(a,c,myFunc);
	cout << sum << endl;
}

int main()
{
	int a = 0;
	char c = 'k';
	float f = 1.0;

	func(a,c,f,testFunc);
}

结果截图:
在这里插入图片描述

还有一种方式,就是使用std::function,需要包含头文件<functional>,std::function 可以用来包装函数、函数指针和lambda表达式等可调用对象。

std::function<T<S...>>表示返回类型为T,参数为S...(可以有多个入参)的函数。

上面的方法可以这样写:

std::function<int(int,char,float)> callback;

int testFunc(int a, char c, float f)
{
    cout << a;
    cout << c;
    cout << f << endl;

    return a;
}

int main()
{

	int a = 0;
	char c = 'k';
	float f = 1.0;

	callback = testFunc;

	int sum = callback(a,c,f);
	cout << a << endl;
}

九、返回指向函数的指针

  我们一般是无法返回函数的,只能通过返回函数指针的方法返回。所以我们要将返回类型写成指针形式,由于编译器不会自动将函数返回类型当作对应的指针类型处理。所以,想要声明一个返回函数指针的函数,最简单的方法是使用类型别名。下面这种写法是错的:

int (*)(int,char,float) retFunc() // 错误
{
	return testFunc;
}

using F = int (int,char,float); F为函数类型,并非为函数指针类型(为啥我就不说了,上面内容有)
using PF = (int*)(int,char,float) PF为函数指针类型

像下面这样定义就是正确的:

PF retFunc() // 错误
{
	return testFunc;
}

我们可以调用一下试一试:

int testFunc(int a, char c, float f)
{
    cout << a;
    cout << c;
    cout << f << endl;

    return a;
}

using PF = int(*)(int,char,float);

PF retFunc(){
    return testFunc;
}

int main()
{
    pf = retFunc();
    int sum = pf(0,'k',1);
    cout << sum << endl;
}

结果截图:
在这里插入图片描述