- C++学习(二)
- C++学习(三)
目录
- 1.sizeof运算符
- 2. 虚基类
- 3.虚函数
- 4.const
- 5.引用
- 6.前置++和后置++的区别
- 7.左值/右值
- 8.单引号和双引号
- 9.getline
- 10.读取数量不定的输入数据
- 11.string常用函数
- 12.new()
- 13.C++中对象new出来和直接声明的区别
- 14.函数参数为void和没有参数的区别
- 15.const与static、const在函数前与函数后区别
- 16.void *memset(void *s, int ch, size_t n);
- 17.数组声名后不初始化,数组里的值都是0吗?
- 18.类型转换
- 19.野指针
- 20. 链表头节点、头指针
- 21.new和malloc的区别
- 22.n>>1 n<<1 和 n/2的区别
- 23.空字符串(“”)和null和空格字符串(" ")的区别
- 24.null 和nullptr
- 25. 二维数组求行数和列数以及vector容器中矩阵求行数和列数
- 26 动态规划中开二维数组相关(剑指offer T47)
- 27 c++中 try 和catch的用法
- 28 C++中的atoi()和stoi()函数的用法和区别
- 29 typedef & define
1.sizeof运算符
(1) 定义
sizeof是一个操作符(operator)。
其作用是返回一个对象或类型所占的内存字节数。(返回一条表达式或者一个类型名字所占的字节数。——C++ Primer)
(2) 语法
sizeof有三种语法形式:
1) sizeof (object); //sizeof (对象)
2) sizeof object; //sizeof 对象
3) sizeof (type_name); //sizeof (类型)
对象可以是各种类型的变量,以及表达式(一般sizeof不会对表达式进行计算)。
sizeof 对象, 返回的是表达式结果类型的大小。
sizeof (表达式); //存贮某类型的对象所占的空间的大小
int i;
sizeof(int); //值为4
sizeof(i); //值为4,等价于sizeof(int)
sizeof i; //值为4
sizeof(2); //值为4,等价于sizeof(int),因为2的类型为int
sizeof(2 + 3.14); //值为8,等价于sizeof(double),因为此表达式的结果的类型为double
char ary[sizeof(int) * 10]; //OK,编译无误
(3) 数组的sizeof
数组的sizeof值等于数组所占用的内存字节数。
注意:1)当字符数组表示字符串时,其sizeof值将’/0’计算进去。
2)当数组为形参时,其sizeof值相当于指针的sizeof值。
(1)
char a[10];
char n[] = "abc";
cout<<"char a[10] "<<sizeof(a)<<endl;//数组,值为10
cout<<"char n[] = /"abc/" "<<sizeof(n)<<endl;//字符串数组,将'/0'计算进去,值为4
---------------------------------------------------------------------------------------
(2)
void func(char a[3])
{
int c = sizeof(a); //c = 4,因为这里a不在是数组类型,而是指针,相当于char *a。
}
void funcN(char b[])
{
int cN = sizeof(b); //cN = 4,理由同上。
}
所谓数组的长度是:数组里面有多少个成员。
比如int a[4] 我现在需要的答案是4 (有可能是空的0,但是我就是要4,不比string)
#include <stdio.h>
int arr[4]={1,2,3};
int main(int argc, char const *argv[]) //char //int
{
printf("%d\n",sizeof(arr) ); //4 //16
printf("%d\n",sizeof(arr[0]) ); //1 //4
printf("%d\n",sizeof(arr)/sizeof(arr[0]) ); //4 //4
}
结论:不要使用sizeof 而要使用sizeof/sizeof 避免数据类型带来的错误
(4) 指针的sizeof()
指针是用来记录另一个对象的地址,所以指针的内存大小当然就等于计算机内部地址总线的宽度。
在32位计算机中,一个指针变量的返回值必定是4。
指针变量的sizeof值与指针所指的对象没有任何关系。
char *b = "helloworld";
char *c[10];
double *d;
int **e;
void (*pf)();
cout<<"char *b = /"helloworld/" "<<sizeof(b)<<endl;//指针指向字符串,值为4
cout<<"char *b "<<sizeof(*b)<<endl; //指针指向字符,值为1
cout<<"double *d "<<sizeof(d)<<endl;//指针,值为4
cout<<"double *d "<<sizeof(*d)<<endl;//指针指向浮点数,值为8
cout<<"int **e "<<sizeof(e)<<endl;//指针指向指针,值为4
cout<<"char *c[10] "<<sizeof(c)<<endl;//指针数组,值为40
cout<<"void (*pf)(); "<<sizeof(pf)<<endl;//函数指针,值为4
2. 虚基类
定义:当在多条继承路径上有一个公共的基类,在这些路径中的某几条汇合处,这个公共的基类就会产生多个实例(或多个副本),若只想保存这个基类的一个实例,可以将这个公共基类说明为虚基类。(百度百科)
虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的 A 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。
在类的继承中,如果我们遇到这种情况:
“B和C同时继承A,而B和C都被D继承”
在此时,假如A中有一个函数fun()当然同时被B和C继承,而D按理说继承了B和C,同时也应该能调用fun()函数。这一调用就有问题了,到底是要调用B中的fun()函数还是调用C中的fun()函数呢?在C++中,有两种方法实现调用:
(注意:这两种方法效果是不同的)
使用作用域标识符来唯一表示它们比如:B::fun()
另一种方法是定义虚基类,使派生类中只保留一份拷贝。
3.虚函数
3.1 虚函数定义
- 虚函数的作用是实现动态绑定的,也就是说程序在运行的时候动态的的选择合适的成员函数。
3.2 虚析构函数
当析构函数不是虚函数时:
基类指针指向了派生类对象,而基类中的析构函数是非virtual的,而虚构函数是动态绑定的基础。现在析构函数不是virtual的,因此不会发生动态绑定,而是静态绑定,指针的静态类型为基类指针,因此在delete的时候只会调用基类的析构函数,而不会调用派生类的析构函数。这样,在派生类中申请的资源就不会得到释放,就会造成内存泄漏,这是相当危险的:如果系统中有大量的派生类对象被这样创建和销毁,就会有内存不断的泄漏,久而久之,系统就会因为缺少内存而崩溃。
将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。
C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。
首先需要了解 vptr指针和虚函数表的概念,以及这两者的关联。
vptr指针指向虚函数表,执行虚函数的时候,会调用vptr指针指向的虚函数的地址。
当定义一个对象的时候,首先会分配对象内存空间,然后调用构造函数来初始化对象。vptr变量是在构造函数中进行初始化的。又因为执行虚函数需要通过vptr指针来调用。如果可以定义构造函数为虚函数,那么就会陷入先有鸡还是先有蛋的循环讨论中。1 构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的。而在构造一个对象时,由于对象还未构造成功。编译器无法知道对象的实际类型,是该类本身,还是该类的一个派生类,或是更深层次的派生类。无法确定。。。
2 虚函数的执行依赖于虚函数表。而虚函数表在构造函数中进行初始化工作,即初始化vptr,让他指向正确的虚函数表。而在构造对象期间,虚函数表还没有被初始化,将无法进行。
从存储空间角度
虚函数对应一个vtable,这大家都知道,可是这个vtable其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过
vtable来调用,可是对象还没有实例化,也就是内存空间还没有,无法找到vtable,所以构造函数不能是虚函数。从使用角度
虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。
虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。构造函数不需要是虚函数,也不允许是虚函数
因为创建一个对象时我们总是要明确指定对象的类型,尽管我们可能通过实验室的基类的指针或引用去访问它。但析构却不一定,我们往往通过基类的指针来销毁对象。这时候如果析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。从实现上看,vbtl在构造函数调用后才建立,因而构造函数不可能成为虚函数 ;从实际含义上看,在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数);而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有太大的必要成为虚函数
当一个构造函数被调用时,它做的首要的事情之一是初始化它的V P T R。因此,它只能知道它是“当前”类的,而完全忽视这个对象后面是否还有继承者。当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码–既不是为基类,也不是为它的派生类(因为类不知道谁继承它)。
3.3 虚函数动态绑定的实现原理
-
虚函数一般是类的成员函数,在每个类里面会有一个虚函数表;虚表中有当前类的各个虚函数的入口地址;每个类的对象有一个指向当前类的虚表的指针(vptr)。构造函数为对象中的虚指针赋值,通过多态类型的指针或引用调用成员函数时,通过虚指针找到虚表,进而找到所调用的虚函数的入口地址,通过该入口地址调用虚函数。
-
虚函数可以实现类的泛化(我自己的理解),为什么析构函数要用虚函数:将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。
3.4 纯虚函数
4.const
如果const出现在 * 号右边,则表示指针自身是常量(顶层const);如果const出现在 * 号左边,则表示被指物是常量(底层const);如果const出现在*号两边,则表示被指物和指针都是常量。
顶层const(指针常量)用来标明一个变量其本身是一个不可更改的常量。内置类型的const为顶层const。对于指针,被顶层const修改后,不可更改指针指向的对象。一个指针本身添加const限定符就是顶层const,而指针所指的对象添加const限定符就是底层const。
const int i = 1;//顶层const
int *const p = &i;//顶层const,不可更改p本身的值
底层const(常量指针)用来标明一个指针或引用所指向的对象是一个不可更改常量。对于指针和引用,被底层const修改后,不可通过指针或引用修改指针指向的对象值。(可以通过其他方式修改其值)
int i = 1;
const int *p = &i;//底层const
*p = 3;//错误,不可通过被const修饰的指针修改对象值
i = 3;//正确,const指针只影响修饰的对象
执行拷贝操作时,顶层const对于拷贝操作无影响
const int i = 1;
int m = i;//i具有顶层const对于拷贝操作无影响。
但是底层const不可忽略。执行拷贝操作时,拷入与拷出对象必须具有相同的底层const,或者两对象的数据类型必须能够转换。一般来说,非常量可以转换成常量,反之不行。
int i = 1;
const int *p = &i;//正确,非常量转换为常量
int *q = p;//错误,常量不可转换为非常量
const int *r = p;//正确,等号两边都具有底层const
5.引用
引用 (& )是标识符的别名;引用变量是一个别名,也就是说,它是某个已存在变量的另一个名字。一旦把引用初始化为某个变量,就可以使用该引用名称或变量名称来指向变量。
定义一个引用时,必须同对它进行初始化使指向已存在的象。
- 把引用作为参数
int i = 17;
int& r = i;
double& s = d;
在这些声明中,& 读作引用。因此,第一个声明可以读作 “r 是一个初始化为 i 的整型引用”,第二个声明可以读作 “s 是一个初始化为 d 的 double 型引用”。
通过使用引用来替代指针,会使 C++ 程序更容易阅读和维护。C++ 函数可以返回一个引用,方式与返回一个指针类似。当函数返回一个引用时,则返回一个指向返回值的隐式指针。这样,函数就可以放在赋值语句的左边。
6.前置++和后置++的区别
从操作符重载的角度,看i++和++i的区别,是一个比较好的切入点。
class Age
{
public:
Age& operator++() //前置++
{
++i;
return *this;
}
const Age operator++(int) //后置++
{
Age tmp = *this;
++(*this); //利用前置++
return tmp;
}
Age& operator=(int i) //赋值操作
{
this->i = i;
return *this;
}
private:
int i;
};
从上述代码,我们可以看出前置++和后置++,有3点不同:
**返回类型不同:**前置++的返回类型是Age&,后置++的返回类型const Age。这意味着,前置++返回的是右值,后置++返回的是右值。
左值和右值,决定了前置++和后置++的用法。
**形参不同:**前置++没有形参,而后置++有一个int形参,但是该形参也没有被用到。很奇怪,难道有什么特殊的用意?其实也没有特殊的用意,只是为了绕过语法的限制 。
**代码不同:**前置++的实现比较简单,自增之后,将this返回即可。需要注意的是,一定要返回this。后置++的实现稍微麻烦一些。因为要返回自增之前的对象,所以先将对象拷贝一份,再进行自增,最后返回那个拷贝。
**效率不同:**如果不需要返回自增之前的值,那么前置++和后置++的计算效果都一样。但是,我们仍然应该优先使用前置++,尤其是对于用户自定义类型的自增操作。前置++的效率更高,理由是:后置++会生成临时对象。
7.左值/右值
有些变量既可以当左值右可以当右值。
左值(Lvalue) →→ Location
表示内存中可以寻址,可以给它赋值(const类型的变量例外)
右值Rvalue) →→ Read
表示可以知道它的值(例如常数)
8.单引号和双引号
单引号是字符型, 双引号是字符串型
‘a’表示是一个字符,"a"表示一个字符串相当于’a’+’\0’;
9.getline
此函数可读取整行,包括前导和嵌入的空格,并将其存储在字符串对象中。
1.使用标准输入输出操作符读写string对象。
int main()
{
string s;
cin>>s;
cout<<s<<endl;
return 0;
}
这样的话,如果输入字符串带有空格(非字符串首部、尾部),则只能输出空格前部分。
而且,读取并忽略开头所有的空白字符。
2.读入未知数目的string对象
string的输入操作符和内置类型的输入操作符一样,也会返回所读的数据流。因此,可以把输入操作作为判断条件:
int main()
{
string s;
while(cin>>s)
cout<<s<<endl;
return 0;
}
当未到达文件尾且未遇到无效输入,则执行循环体,并将读取到的字符串输出到标准输出。如果到达文件尾,则跳出while循环。
3.使用getline读取整行文本
getline(cin,strings);
getline从输入流的下一行读取,并保存读取的内容到string中,但不包括换行符。即便它是第一个字符,也会终止读入,并返回。
int main()
{
string s;
while(getline(cin,s))
cout<<s<<endl;
return 0;
}
getline(cin, inputLine);
10.读取数量不定的输入数据
#include <iostream>
int main()
{
int sum = 0, value = 0;
//读取数据直到遇到文件尾,计算所有输入的值的和
while(cin >> value)//此处是重点
{
sum += value;
}
cout << " sum is: "<< sum <<endl;
return 0;
}
while循环的判据就是表达式cin >> value,这个表达式代表从标准输入中读取下一个数,保存在value中。输入运算符返回的是其左侧的对象,即cin。故这个循环检测的实际上是cin。
当使用一个istream(即cin)对象作为条件时,其效果是检测流的状态。若流有效,没有遇到错误,那么检测成功。当遇到**文件结束符或者无效输入(**本例中无效输入为非整型)时,cin会处于无效状态,循环会停止。
11.string常用函数
12.new()
“new”是C++的一个关键字,同时也是操作符。
定义:
new 类型名T(初始化参数列表)
功能:在程序执行期间,申请用于存放T类型对象的内存空间,并依初值列表赋以
初值。
结果值:成功:T类型的指针,指向新分配的内存;失败:抛出异常。
释放内存操作符delete
delete 指针p
功能:释放指针p所指向的内存。p必须是new操作的返回值。
new其实就是告诉计算机开辟一段新的空间,但是和一般的声明不同的是,new开辟的空间在堆上,而一般声明的变量存放在栈上。通常来说,当在局部函数中new出一段新的空间,该段空间在局部函数调用结束后仍然能够使用,可以用来向主函数传递参数。另外需要注意的是,new的使用格式,new出来的是一段空间的首地址。所以一般需要用指针来存放这段地址。具体的代码如下:
#include <iostream>
using namespace std;
int example1()
{
//可以在new后面直接赋值
int *p = new int(3);
//也可以单独赋值
//*p = 3;
//如果不想使用指针,可以定义一个变量,在new之前用“*”表示new出来的内容
int q = *new int;
q = 1;
cout << q << endl;
return *p;
}
int* example2()
{
//当new一个数组时,同样用一个指针接住数组的首地址
int *q = new int[3];
for(int i=0; i<3; i++)
q[i] = i;
return q;
}
struct student
{
string name;
int score;
};
student* example3()
{
//这里是用一个结构体指针接住结构体数组的首地址
//对于结构体指针,个人认为目前这种赋值方法比较方便
student *stlist = new student[3]{{"abc", 90}, {"bac", 78}, {"ccd", 93}};
return stlist;
}
int main()
{
int e1 = example1();
cout <<"e1: "<< e1 << endl;
int *e2 = example2();
for(int i=0; i<3; i++)
cout << e2[i] << " ";
cout << endl;
student *st1 = example3();
for(int i=0; i<3; i++)
cout << st1[i].name << " " << st1[i].score << endl;
return 0;
}
13.C++中对象new出来和直接声明的区别
(1)首先,最直观的,new出来的对象需要使用指针接收,而直接声明的不用。例如 A* a=new A() 与A a()。
(2)new出来的对象是直接使用堆空间,而局部声明一个对象是放在栈中。
new出来的对象类似于申请空间,因此需要delete销毁,而直接声明的对象则在使用完直接销毁。
(3)new出来的对象的生命周期是具有全局性,譬如在一个函数块里new一个对象,可以将该对象的指针返回回去,该对象依旧存在。而声明的对象的生命周期只存在于声明了该对象的函数块中,如果返回该声明的对象,将会返回一个已经被销毁的对象。
(4)new对象指针用途广泛,比如作为函数返回值、函数参数等``。
14.函数参数为void和没有参数的区别
【参考】
C语言中的函数在声明和定义的时候可以没有参数。众所周知,如果函数被声明和定义为void f(void);则说明该函数在调用时不能传入任何参数。而如果函数被声明和定义为void f();则说明该函数在调用时候可以传入任意参数。
如果函数无参数,那么应声明其参数为 void
C++中没有区别。
在 C++语言中声明一个这样的函数:
int function(void) { return 1; }
则进行下面的调用是不合法的:function(2);
因为在 C++中,函数参数为 void的意思是这个函数不接受任何参数。
15.const与static、const在函数前与函数后区别
const在函数前与函数后区别
在普通的非 const成员函数中,this的类型是一个指向类类型的 const指针。可以改变this所指向的值,但不能改变 this所保存的地址。
在 const成员函数中,this的类型是一个指向 const类类型对象的 const指针。既不能改变 this所指向的对象,也不能改变 this所保存的地址。
任何不会修改数据成员的函数都应该声明为const类型。如果在编写const成员函数时,不慎修改了数据成员,或者调用了其它非const成员函数,编译器将指出错误,这无疑会提高程序的健壮性。
const 成员函数的声明看起来怪怪的:const关键字只能放在函数声明的尾部,大概是因为其它地方都已经被占用了。
关于Const函数的几点规则:
const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数.
const对象的成员是不可修改的,然而const对象通过指针维护的对象却是可以修改的.
const成员函数不可以修改对象的数据,不管对象是否具有const性质.它在编译时,以是否修改成员数据为依据,进行检查.
然而加上mutable修饰符的数据成员,对于任何情况下通过任何手段都可修改,自然此时的const成员函数是可以修改它的
举例:
1、int GetY() const;
2、const int * GetPosition();
对于1
该函数为只读函数,不允许修改其中的数据成员的值。
对于2
修饰的是返回值,表示返回的是指针所指向值是常量。
static和const的区别
能不能同时用static和const修饰类的成员函数?
不可以。
解释:1:C++编译器在实现const的成员函数时为了确保不能通过该函数修改对象的状态,会在函数中添加一个隐式的参数 this指针(类名 const* const this)。一个成员函数为static的时候,该函数是没有this指针的。此时const的用法和static是冲突的。
解释2:语意矛盾。static的作用是表示该函数只作用在类型的静态变量上,与类的实例无关;const的作用是确保函数不能修改类的实例的状态,与类型的静态变量无关。因此不能同时使用。
16.void *memset(void *s, int ch, size_t n);
memset是计算机中C/C++语言初始化函数。作用是将某一块内存中的内容全部设置为指定的值, 这个函数通常为新申请的内存做初始化工作。
函数解释:将s中当前位置后面的n个字节 (typedef unsigned int size_t )用 ch 替换并返回 s 。
memset:作用是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法 。
memset()函数原型是extern void *memset(void *buffer, int c, int count) buffer:为指针或是数组,c:是赋给buffer的值,count:是buffer的长度.
# include <stdio.h>
# include <string.h>
int main(void)
{
int i; //循环变量
char str[10];
char *p = str;
memset(str, 0, sizeof(str)); //只能写sizeof(str), 不能写sizeof(p)
for (i=0; i<10; ++i)
{
printf("%d\x20", str[i]);
}
printf("\n");
return 0;
}
/*
根据memset函数的不同,输出结果也不同,分为以下几种情况:
memset(p, 0, sizeof(p)); //地址的大小都是4字节
0 0 0 0 -52 -52 -52 -52 -52 -52
memset(p, 0, sizeof(*p)); //*p表示的是一个字符变量, 只有一字节
0 -52 -52 -52 -52 -52 -52 -52 -52 -52
memset(p, 0, sizeof(str));
0 0 0 0 0 0 0 0 0 0
memset(str, 0, sizeof(str));
0 0 0 0 0 0 0 0 0 0
memset(p, 0, 10); //直接写10也行, 但不专业
0 0 0 0 0 0 0 0 0 0
*/
17.数组声名后不初始化,数组里的值都是0吗?
1、全局/静态数组
如果申明的是全局/静态数组,系统会把数组的内容自动初始化为0。
2、局部数组
如果申明的是局部数组,数组的内容会是随机的,不一定是0。如函数内声明:
int Func()
{
char szTest[10]; //此时内容是随机的
memset(szTest, 0, sizeof(szTest));
}
3、成员数据
如果申明的是类的成员数组,数组的内容是随机的,不一定是0。一般在类的构造函数内用memset初始化为0。
18.类型转换
C++类型转换通常有三种不同的形式。可以分为“旧式转型”和“新式转型”。
旧式转型
C-style转型:(1)(T)expression //将expression转型为T
函数风格: (2)T(expression) //将expression转型为T
新式转型:
C++提供了四种新式转型,为什么还要引入这四种新式转换呢?原因:第一:它们很容易在代码中被辨识出来(不论是人工辨识还是工具),因而得以简化“找出类型系统在哪个地方被破坏”的过程。第二:各转型动作的目标愈窄化,编译器愈可能诊断出错误的运行。
const_cast:通常被用来将对象的常量性转除。它也是唯一有此能力的C++_style转型操作符。
static_cast:主要用于C++内置数据类型之间的转换,但是没有运行时类型的检测来保证转换的安全性,用于基类和子类之间的指针或引用的转换。这种转换把子类的指针或引用转换为基类表示是安全的;把基类的指针或引用转换成子类表示是不安全的,没有进行动态类型检测,即下行转换不安全。
dynamic_cast:动态类型转型,主要用于“安全向下转型”,在转换时会进行类型安全检测。dynamic_cast转换成功的话会返回类的指针或引用,失败返回NULL;dynamic_cast进行转换的时候基类中一定要有虚函数,这样才有意义;在类的转换时,在类层次间进行转换的时候,dynamic_cast和static_cast进行上行转换的效果一样,但在进行下行转换的时候,dynamic_cast会进行类型安全检查,所以它更安全;它可以让指向基类的指针转换为指向其子类的指针或是其兄弟类的指针。
reinterpret_cast:重解释类型转换。它有着和C风格强制类型转换同样的功能;它可以转换任何的内置数据类型为其他的类型,同时它也可以把任何类型的指针转化为其他的类型;
19.野指针
野指针,就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)很可能触发运行时段错误(Sgmentation fault)
我们在删除一个指针之后,编译器只会释放该指针所指向的内存空间,而不会删除这个指针本身。
[在删除一个指针之后,一定将该指针设置成空指针(即在delete *p之后一定要加上: p=NULL)]
(https://blog.csdn.net/qq_36570733/article/details/80043321)
1.为什么指针变量定义时一定要初始化?
答:因为你首先要理解一点,内存空间不是你分配了才可以使用,只是你分配了之后使用才安全,为什么要进行对他初始化呢?因为,如果你没对他初始化,而引用这个指针并却其指向的内存进行修改。因为指针未被初始化,所以指针所指向的也是随机的,他是个野指针,如果你引用指针,并修改这个指针所指向的内容,而如果这个指针所指向的内容恰好是另外一个程序的数据的话,你将其进行修改了,就会导致另外一个程序可能不能正常运行了.所以使用前一定要进行初始化
2.指针变量初始化为NULL是什么意思?
答:意思是说,强指针变量置空,初始化为NULL,使它不指向任何内容,这样引用她也不会出现上面的问题
总之一点,记住在使用指针之前要对它进行初始化操作就可以了
20. 链表头节点、头指针
链表中第一个结点的存储位置叫做头指针,那么整个链表的存取就必须是从头指针开始进行了。之后的每一个结点,其实就是上一个的后继指针指向的位置。
这里有个地方要注意,就是对头指针概念的理解,这个很重要。“链表中第一个结点的存储位置叫做头指针”,如果链表有头结点,那么头指针就是指向头结点数据域的指针。
头指针就是链表的名字。头指针仅仅是个指针而已。
- 头结点是为了操作的统一与方便而设立的,放在第一个元素结点之前,其数据域一般无意义(当然有些情况下也可存放链表的长度、用做监视哨等等)。
- 有了头结点后,对在第一个元素结点前插入结点和删除第一个结点,其操作与对其它结点的操作统一了。
- 首元结点也就是第一个元素的结点,它是头结点后边的第一个结点。
- 头结点不是链表所必需的。
- 头指针具有标识作用,故常用头指针冠以链表的名字。
- 无论链表是否为空,头指针均不为空。头指针是链表的必要元素。
举例:
1.链表添加函数中为什么要用指向链表指针的指针
2.剑指offer链表添加,删除元素中传入的pHead为什么要是指向头指针的指针问题
以指针作为实参时,实参p传递给形参p1的其实是p的拷贝,所以在局部函数中改变形参p1的指向对身处主函数的p是无影响的,但是因为p1是p的拷贝,所以他们的指向是相同的,所以可以通过p1修改了那块内存的值。
21.new和malloc的区别
new和malloc的区别是C/C++一道经典的面试题,我也遇到过几次,回答的都不是很好,今天特意整理了一下。
- 属性
new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持。
- 参数
使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。
- 返回类型
new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
- 分配失败
new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。
- 自定义类型
new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。
malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
- 重载
C++允许重载new/delete操作符,特别的,布局new的就不需要为对象分配内存,而是指定了一个地址作为内存起始区域,new在这段内存上为对象调用构造函数完成初始化工作,并返回此地址。而malloc不允许重载。
- 内存区域
new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中。
new分配对象内存的三个步骤:
(1)调用operator new()分配内存;
(2)调用构造函数来构造对象并传入初值;
(3)返回指向该对象的指针。
delete释放对象内存的两个步骤:
(1)调用析构函数;
(2)调用operator delete释放内存。
参考:
【1】
22.n>>1 n<<1 和 n/2的区别
-
右移运算符 (需要移位的数字 >> 移位的次数)
将需要移位的数字转化成二进制,将转化完的数字整体向右移动对应位移位数,低位舍弃,高位的空位补符号位(正数补零,负数补1)。
例:12 >> 2; 12的二进制为1100,将1100整体右移两个单位,因为12为正数,高位填零,变成0011,是十进制中的3. -
左移运算符 (需要移位的数字 << 移位的次数)
将需要移位的数字转化成二进制,将转化完的数字整体向左移动对应位移位数,高位舍弃,低位的空位补0.
例:5<<2; 5的二进制为0101,将0101整体左移两个单位,低位补0,得10100,十进制中的20。在进行移位的时候,可能存在溢出问题。 -
/——除法
结果=被除数/除数
被除数 除数 结果
浮点数 浮点数 浮点数
浮点数 整数 浮点数
整数 整数 整数
整数 浮点数 浮点数
注:5/2的值为2(整除),而用(double)5/2的值则为2.5。
5%2=1(取余)
>> 和 / 的区别:
- 操作对象类型不同
右移运算符只有整数才能使用
/ 整数和浮点数都能够使用。 - 运算效率不同
通常情况下,右移操作比整数除法快。但涉及到浮点数的除法速度是最慢的。 - 优先级不同
右移运算符比 / 的优先级低,两者同时参与运算,先计算乘除,后计算左移或右移。
23.空字符串(“”)和null和空格字符串(" ")的区别
1、类型
null表示的是一个对象的值,而并不是一个字符串。
例如声明一个对象的引用,String a = null ;
""表示的是一个空字符串,也就是说它的长度为0,但它是一个字符串。
例如声明一个字符串String str = “” ;
2、内存分配
String str = null ; 表示声明一个字符串对象的引用,但指向为null,也就是说还没有指向任何的内存空间;
String str = “”; 表示声明一个字符串类型的引用,其值为""空字符串,这个str引用指向的是空字符串的内存空间;
“” :分配了内存 ,分配了一个空间
null :未分配内存
" " :分配了内存,分配了一个空间
string str1 = ""; //空字符串 str1.length() 等于 0
string str2 = null; //NULL
string str3 = " "; //空格串 str2.length() 等于 1
1.1 字符
(1)首先必须明确字符型(char)是整数类型,其在内存单元是以整数形式存放。
(2)其次,char类型的产生是为了用于:存储字母、数字、标点字符、非打印字符。
(3) 为方便处理字符,用特定的整数表示特定字符,即我们看到的编码。实质上就是一种转化代替的思想,用这种编码从而去描述字符,最常用的是ASCII码。
1.1.1 空字符 空格字符
空字符: 字符串结尾的标志(‘\0’),实际上他的数值是0。 可以理解为标志性字符型,其使命主要是为了表明字符串已经结束。
空格字符: 空格字符( ‘ ’单引号中间有一个空格)的ASCII码10进制32,16进制的0X20
两者区别:
最直观的区别:值不同两者的ASCII不同,空(NUL)字符码值是0,而空格字符的码值是32。再者,空字符人为规定了它的使命。
1.2 字符串
字符串:字符串属于字符类型的派生类型(char数组)。用于字符串一定要以空字符(‘\0’)结束,故所有的字符串里面一定有一个空字符。当然空字符串(“”)也不例 外。
空格字符串“”。
字符与字符串因为是两种不同的类型,所以也容易区分,这里就不在啰嗦。
1.3 NULL
NULL:值为0,空值。NULL是空地址,不占用任何字节,主要是是用来给指针赋值的。其实就是0地址,这个地址在C语言里面是不允许访问的,访问会出异常。NULL一般用来初始化指针变量。例如:
char *str = NULL; 表明该变量不指向任何有效的内存区域,避免野指针。
注意以下几点:
(1)从Stdio.h 中我们可以看出:C++中 NULL为(int)0 ,而在 C中NULL为( void* )0。据此可知在C和C++中NULL宏的值有所不同。
(2)C程序中NULL == ‘\0’为真 , 只是因为’\0’也是数值0而已,两者并不是一个意思,千万别搞混了。
(3)NULL 可以赋值给任意类型变量,相应值为空
(4)为编程规范,在定义指针时,一般需要初始化,常用NULL来初始化。
int *p = NULL,相比直接定义int *p 而言,int *p未初始化,p是一个野指针,保存的是一个随机值 ; int *p=NULL 已经初始化,指向一个空指针。
int *p = NULL等价于于 int *p= 0,p的值是 0x00;int * q ,q的值是一个随机值。
24.null 和nullptr
在C中,习惯将NULL定义为void*指针值0:
#define NULL (void*)0
但同时,也允许将NULL定义为整常数0
在C++中,NULL却被明确定义为整常数0: #define NULL 0
25. 二维数组求行数和列数以及vector容器中矩阵求行数和列数
vector数组
vector<vector > matrix;
行数:matrix.size();
列数:matrix[0].size();
二维数组:
int matrix[ ][ ];
a = sizeof(matrix[0][0]);//一个元素占得空间
b= sizeof(matrix[0]);//一行所占的空间
c= sizeof(matrix);//整个数组占得空间
行数: sizeof(matrix) /sizeof(matrix[0])
列数:sizeof(matrix[0]) / sizeof(matrix[0][0])
26 动态规划中开二维数组相关(剑指offer T47)
https://leetcode-cn.com/problems/li-wu-de-zui-da-jie-zhi-lcof/
1. new写法
//开二维数组
int** dp = new int* [rows];
for(int i = 0; i < rows; ++i)
dp[i] = new int[cols];
//释放内存
for(int i = 0; i < rows; ++i)
delete[] dp[i]; //释放dp[i]指向的内存(低一维的)
delete[] dp;释放dp指向的内存(高一维的)
2. vector写法
vector<vector> dp(m, vector(n,0));
27 c++中 try 和catch的用法
#include<iostream> //包含头文件
using namespace std;
double fuc(double x, double y) //定义函数
{
if(y==0)
{
throw y; //除数为0,抛出异常
}
return x/y; //否则返回两个数的商
}
int main()
{
double res;
try //定义异常
{
res=fuc(2,3);
cout<<"The result of x/y is : "<<res<<endl;
res=fuc(4,0); //出现异常
}
catch(double) //捕获并处理异常
{
cerr<<"error of dividing zero.\n";
exit(-1); //异常退出程序
}
}
catch 的数据类型需要与throw出来的数据类型相匹配的。
catch(…)能匹配成功所有的数据类型的异常对象,包括C++语言提 供所有的原生数据类型的异常对象,如int、double,还有char*、int*这样的指针类型,另外还有数组类型的异常对象。同时也包括所有自定义 的抽象数据类型。
Reference
1)若有异常则通过throw操作创建一个异常对象并抛掷。
2)将可能抛出异常的程序段嵌在try块之中。控制通过正常的顺序执行到达try语句,然后执行try块内的保护段。
3)如果在保护段执行期间没有引起异常,那么跟在try块后的catch子句就不执行。程序从try块后跟随的最后一个catch子句后面的语句继续执行下去。
4) catch子句按其在try块后出现的顺序被检查。匹配的catch子句将捕获并处理异常(或继续抛掷异常)。
5)如果匹配的处理器未找到,则运行函数terminate将被自动调用,其缺省功能是调用abort终止程序。
6)处理不了的异常,可以在catch的最后一个分支,使用throw语法,向上扔。
#include <iostream>
using namespace std;
double division(int a, int b)
{
if( b == 0 )
{
throw "Division by zero condition!";
}
return (a/b);
}
int main ()
{
int x = 50;
int y = 0;
double z = 0;
try {
z = division(x, y);
cout << z << endl;
}
catch (const char* msg) {
cerr << msg << endl;
}
return 0;
}
28 C++中的atoi()和stoi()函数的用法和区别
相同点:
①都是C++的字符处理函数,把数字字符串转换成int输出
②头文件都是#include
不同点:
①atoi()的参数是 const char* ,因此对于一个字符串str我们必须调用 c_str()的方法把这个string转换成 const char类型的,而stoi()的参数是const string,不需要转化为 const char*;
②stoi()会做范围检查,默认范围是在int的范围内的,如果超出范围的话则会runtime error,而atoi()不会做范围检查,如果超出范围的话,超出上界,则输出上界,超出下界,则输出下界;
#include "stdafx.h"
#include <iostream>
#include <set>
#include <string>
using namespace std;
int main(){
string s1 = "2147482", s2 = "-214748";
string s3 = "214748666666663", s4 = "-21474836488";
cout << stoi(s1) << endl;
cout << stoi(s2) << endl;
cout << atoi(s3.c_str()) << endl;
cout << atoi(s4.c_str()) << endl;
return 0;
}
Reference
https://blog.csdn.net/qq_33221533/article/details/82119031
29 typedef & define
一、typedef的用法
在C/C++语言中,typedef常用来定义一个标识符及关键字的别名,它是语言编译过程的一部分,但它并不实际分配内存空间,实例像:
typedef int INT;
typedef int ARRAY[10];
typedef (int*) pINT;
typedef可以增强程序的可读性,以及标识符的灵活性,但它也有“非直观性”等缺点。
二、define的用法
#define为一宏定义语句,通常用它来定义常量(包括无参量与带参量),以及用来实现那些“表面似和善、背后一长串”的宏,它本身并不在编译过程中进行,而是在这之前(预处理过程)就已经完成了,但也因此难以发现潜在的错误及其它代码维护问题,它的实例像:
#define INT int
#define TRUE 1
#define Add(a,b) ((a)+(b));
#define Loop_10 for (int i=0; i<10; i++)
在Scott Meyer的Effective C++一书的条款1中有关于#define语句弊端的分析,以及好的替代方法,大家可参看。
三、异同
- 相同点
typedef和define都可以用来给对象取一个别名. - 不同点
首先,二者执行时间不同
关键字typedef在编译阶段有效,由于是在编译阶段,因此typedef有类型检查的功能。
Define则是宏定义,发生在预处理阶段,也就是编译之前,它只进行简单而机械的字符串替换,而不进行任何检查。
四、typedef的四个用途和两个陷阱
(1) 四个用途
-
用途一: 定义一种类型的别名,而不只是简单的宏替换。可以用作同时声明指针型的多个对象。
比如:
char* pa, pb; // 这多数不符合我们的意图,它只声明了一个指向字符变量的指针,
// 和一个字符变量;
以下则可行:
typedef char* PCHAR; // 一般用大写
PCHAR pa, pb; // 可行,同时声明了两个指向字符变量的指针
虽然:
char *pa, *pb;
也可行,但相对来说没有用typedef的形式直观,尤其在需要大量指针的地方,typedef的方式更省事。 -
用途二: 用在旧的C代码中(具体多旧没有查),帮助struct。
以前的代码中,声明struct新对象时,必须要带上struct,即形式为: struct 结构名 对象名,如:
struct tagPOINT1
{
int x;
int y;
};
struct tagPOINT1 p1;而在C++中,则可以直接写:结构名 对象名,即:
tagPOINT1 p1;估计某人觉得经常多写一个struct太麻烦了,于是就发明了:
typedef struct tagPOINT
{
int x;
int y;
}POINT;POINT p1; // 这样就比原来的方式少写了一个struct,比较省事,尤其在大量使用的时候
或许,在C++中,typedef的这种用途二不是很大,但是理解了它,对掌握以前的旧代码还是有帮助的,毕竟我们在项目中有可能会遇到较早些年代遗留下来的代码。
-
用途三: 用typedef来定义与平台无关的类型。
比如定义一个叫 REAL 的浮点类型,在目标平台一上,让它表示最高精度的类型为:
typedef long double REAL;
在不支持 long double 的平台二上,改为:
typedef double REAL;
在连 double 都不支持的平台三上,改为:
typedef float REAL;
也就是说,当跨平台时,只要改下 typedef 本身就行,不用对其他源码做任何修改。
标准库就广泛使用了这个技巧,比如size_t。
另外,因为typedef是定义了一种类型的新别名,不是简单的字符串替换,所以它比宏来得稳健(虽然用宏有时也可以完成以上的用途)。 -
用途四: 为复杂的声明定义一个新的简单的别名。方法是:在原来的声明里逐步用别名替换一部分复杂声明,如此循环,把带变量名的部分留到最后替换,得到的就是原声明的最简化版。举例:
-
原声明:int *(a[5])(int, char);
变量名为a,直接用一个新别名pFun替换a就可以了:
typedef int *(pFun)(int, char);
原声明的最简化版:
pFun a[5]; -
原声明:void (b[10]) (void ()());
变量名为b,先替换右边部分括号里的,pFunParam为别名一:
typedef void (*pFunParam)();
再替换左边的变量b,pFunx为别名二:
typedef void (*pFunx)(pFunParam);
原声明的最简化版:
pFunx b[10]; -
原声明:doube(*)() (*e)[9];
变量名为e,先替换左边部分,pFuny为别名一:
typedef double(*pFuny)();
再替换右边的变量e,pFunParamy为别名二
typedef pFuny (*pFunParamy)[9];
原声明的最简化版:
pFunParamy e;理解复杂声明可用的“右左法则”:从变量名看起,先往右,再往左,碰到一个圆括号就调转阅读的方向;括号内分析完就跳出括号,还是按先右后左的顺序,如此循环,直到整个声明分析完。举例:
int (func)(int p);
首先找到变量名func,外面有一对圆括号,而且左边是一个号,这说明func是一个指针;然后跳出这个圆括号,先看右边,又遇到圆括号,这说明(func)是一个函数,所以func是一个指向这类函数的指针,即函数指针,这类函数具有int类型的形参,返回值类型是int。
int (func[5])(int );
func右边是一个[]运算符,说明func是具有5个元素的数组;func的左边有一个,说明func的元素是指针(注意这里的不是修饰func,而是修饰func[5]的,原因是[]运算符优先级比高,func先跟[]结合)。跳出这个括号,看右边,又遇到圆括号,说明func数组的元素是函数类型的指针,它指向的函数具有int*类型的形参,返回值类型为int。也可以记住2个模式:
type ()(…)函数指针
type ()[]数组指针
(2)两个陷阱
-
陷阱一: typedef是定义了一种类型的新别名,不同于宏,它不是简单的字符串替换。
比如先定义:
typedef char* PSTR;
然后:
int mystrcmp(const PSTR, const PSTR);const PSTR实际上相当于const char吗?不是的,它实际上相当于char const。
原因在于const给予了整个指针本身以常量性,也就是形成了常量指针char* const。
简单来说,记住当const和typedef一起出现时,typedef不会是简单的字符串替换就行。 -
陷阱二: typedef在语法上是一个存储类的关键字(如auto、extern、mutable、static、register等一样),虽然它并不真正影响对象的存储特性如:
typedef static int INT2; //不可行
编译将失败,会提示“指定了一个以上的存储类”。