Introduction - krishnaramb/cplusplus GitHub Wiki

Contents

The Basic

  • C++ offers variety of notations for expressing initialization, such as =
  • Other universal form based on curly-brace-delimited initilier list:
#include <iostream>

using namespace std;

int main()
{
  int  d1{2.5};// gives out compilation error
  int d1 = {2.5}; // same as above, here '=' is redundant
  int d2 = 2.5; // d2 will hold 2
  cout << d1;
}
  • The = form is traditional and dates back in C, but if in doubt, use the general {}-list form. If nothing else, it saves you from the conversions that lose information( narrowing conversion).
  int i1 = 7.2; // i1 becomes 7 (surpr ise?)
  int i2 {7.2}; // error : floating-point to integer conversion
  int i3 = {7.2}; // error : floating-point to integer conversion (the = is redundant)
  • Note that a constant cannot be left uninitialized. A variable should only be left uninitialized in extremely rare condition.

  • user defind types (such as string, vector, Matrix, Motor controller) can be defined to be implicitly initialized? How?? 👊 ---> wait for constructor

  • When defining a variable, you don’t actually need to state its type explicitly when it can be deduced from the initializer:

auto b = true; // a bool
auto ch = 'x'; // a char
auto i = 123; // an int
auto d = 1.2; // a double
auto z = sqrt(y); // z has the type of whatever sqr t(y) retur ns
  • With auto, we use the = because there is no potentially troublesome type conversion involved. 👍

  • We use auto where we don’t hav e a specific reason to mention the type explicitly.We want to be explicit about a variable’s range or precision (e.g., double rather than float). ‘‘Specific reasons’’ include:

    • The definition is in a large scope where we want to make the type clearly visible to readers of our code.
    • We want to be explicit about a variable’s range or precision (e.g., double rather than float).

The important point is, Using auto, we avoid redundancy and writing long type names. This is especially important in generic programming where the exact type of an object can be hard for the programmer to know and the type names can be quite long. (more on iterators)

Constants

C++ support two notion of immutability.

  • const: meaning roughly ‘‘I promise not to change this value.’’ This is used primarily to specify interfaces, so that data can be passed to functions without fear of it being modified. The compiler enforces the promise made by const
  • constexpr: meaning roughly ‘‘to be evaluated at compile time.’’ This is used primarily to specify constants, to allow placement of data in read-only memory (where it is unlikely to be corrupted) and for performance. const int dmv = 17; // dmv is a named constant
const int dmv = 17; // dmv is a named constant
int var = 17; // var is not a constant
constexpr double max1 = 1.4∗square(dmv); // OK if square(17) is a constant expression
constexpr double max2 = 1.4∗square(var); // error : var is not a constant expression
const double max3 = 1.4∗square(var); //OK, may be evaluated at run time
double sum(const vector<double>&); // sum will not modify its argument (§1.8)
vector<double> v {1.2, 3.4, 4.5}; // v is not a constant
const double s1 = sum(v); // OK: evaluated at run time
constexpr double s2 = sum(v); // error : sum(v) not constant expression

Pointers, Arrays, and Loops

char v[6];  // array of 6 characters
char∗p;    //pointer to character
char∗ p = &v[3]; // p points to v’s four th element
char x = ∗p; //*p is the object that p points to

pointer

Consider copying ten elements from one array to another:

//program 1
void copy_fct()
{
int v1[10] = {0,1,2,3,4,5,6,7,8,9};
int v2[10]; // to become a copy of v1
for (auto i=0; i!=10; ++i) // copy elements
v2[i]=v1[i];
// ...
}
//program 2
void print()
{
int v[] = {0,1,2,3,4,5,6,7,8,9};
for (auto x : v) // for each x in v
cout << x << '\n';
for (auto x : {10,21,32,43,54,65})
cout << x << '\n';
// ...
}

Note that we don’t have to specify an array bound when we initialize it with a list. The range-for-statement can be used for any sequence of elements

If we didn’t want to copy the values from v into the variable x, but rather just have x refer to an element, we could write:

void increment()
{
int v[] = {0,1,2,3,4,5,6,7,8,9};
for (auto& x : v)
    ++x;
// ...
}

Explain why the following code doesn't work?

// Example program
#include <iostream>
#include <string>
using namespace std;
void increment(int x[]);
int main()
{
    int a[] = {1, 2,3};
    increment(a);
}

void increment(int array[]){
    for (auto x:array)
        cout<<x;
}

The solution is, when you pass an array to a function, it decays to a pointer. So in the increment function array is just a decayed pointer. It doesn't know the side of array. since the for statement for (auto x:array) requires undecayed array for finding the end of this array, it will not work if arary is a pointer.

In a declaration, the unary suffix & means ‘‘reference to.’’ A reference is similar to a pointer, except that you don’t need to use a prefix to access the value referred to by the reference. Also, a reference cannot be made to refer to a different object after its initialization. More on References.

Important uses of reference

  • References are particularly useful for specifying function arguments. For example
void sort(vector<double>& v); // sort v

By using a reference, we ensure that for a call sort(my_vec), we do not copy my_vec and that it really is my_vec that is sorted and not a copy of it.

When we don’t want to modify an argument, but still don’t want the cost of copying, we use a const reference. For example:

double sum(const vector<double>&)
  • Functions taking const references are very common.
  • When used in declarations, operators (such as &, ∗, and [ ]) are called declarator operators:
T a[n]; // T[n]: array of n Ts
T∗ p; // T*: pointer to T
T& r; // T&: reference to T
T f(A); // T(A): function taking an argument of type A returning a result of type T

Exception and Assertion

Exception reports errors found at the run time. If an error can be found at compile time, it is usually prefable to do so. Assertion will help you catch the error at compile time thus saving time

Function Overloading

  • Many functions with the same name, but different arguments 🌿
  • The function called is the one whose arguments match the invocation
void printOnNewLine(int x)
{
    cout << "Integer: " << x << endl;
}
void printOnNewLine(char *x)
{
    cout << "String: " << x << endl;
}
  • printOnNewLine(3) prints “Integer: 3”
  • printOnNewLine(“hello”) prints “String: hello”
void printOnNewLine(int x)
{
    cout << "1 Integer: " << x << endl;
}
void printOnNewLine(int x, int y)
{
    cout << "2 Integers: " << x << " and " << y << endl;
}
  • printOnNewLine(3) prints “1 Integer: 3”
  • printOnNewLine(2, 3) prints “2 Integers: 2 and 3”
  • Note that function declaration need to occur before invocation
int foo()
{
      return bar()*2; // ERROR - bar hasn’t been declared yet
}
int bar()
{
      return 3;
}
  • Solution 1: reorder function declarations
int bar()
{
    return 3;
}
int foo()
{
    return bar()*2; // ok
}
  • Solution 2: use a function prototype; informs the compiler you’ll implement it later
int bar(); // function prototype
int foo()
{
    return bar()*2; // ok
}
int bar()
{
    return 3;
}
  • Function prototypes should match the signature of the method, though argument names don’t matter
int square(int); //function prototype
int cube(int x)
{
    return x*square(x);
}
int square(int x)
{
    return x*x;
}
  • Function prototypes are generally put into separate header files
    • Separates specification of the function from its implementation
// myLib.h - header
// contains prototypes
int square(int);
int cube (int);
// myLib.cpp - implementation
#include "myLib.h"
int cube(int x)
{
    return x*square(x);
}
int square(int x)
{
    return x*x;
}

Recursion

  • functiosn can call themselves
  • fib(n) = fib(n-1) + fib(n-2) can be easily expressed via a recursive implementation
int fibonacci(int n) {
if (n == 0 || n == 1) {
    return 1;
} else {
    return fibonacci(n-2) + fibonacci(n-1);
    }
}

Pass by value vs by reference

  • passing by value: makes a copy of the variable; changes to the variable within the function don’t occur outside the function
// pass-by-value
void increment(int a) {
    a = a + 1;
    cout << "a in increment " << a << endl;
}
int main() {
    int q = 3;
    increment(q); // does nothing
    cout << "q in main " << q << endl;
  }
  //a in increment 4
//q in main 3
  • If you want to modify the original variable as opposed to making a copy, pass the variable by reference (int &a instead of int a)
// pass-by-value
void increment(int &a) {
    a = a + 1;
    cout << "a in increment " << a << endl;
}
int main() {
    int q = 3;
    increment(q); // works
    cout << "q in main " << q << endl;
}
//output
//a in increment 4
//q in main 4

Implementing Swap

void swap(int &a, int &b) {
    int t = a;
    a = b; // HERE
    b = t;
}
int main() {
    int q = 3;
    int r = 5;
    swap(q, r);
    cout << "q " << q << endl; // q 5
    cout << "r " << r << endl; // r 3
}

Arrays and Strings

So far we have used variables to store values in memory for later reuse. We now explore a means to store multiple values together as one unit, the array.

An array is a fixed number of elements of the same type stored sequentially in memory. Therefore, an integer array holds some number of integers, a character array holds some number of characters, and so on. The size of the array is referred to as its dimension. To declare an array in C++, we write the following:
type arrayName[dimension];

Different ways to initialize an array:

int arr[4];
arr[0] = 6;
arr[1] = 0;
arr[2] = 9;
arr[3] = 6;
int arr[4] = { 6, 0, 9, 6 };
int arr[] = { 6, 0, 9, 6, 2, 0, 1, 1 };
int arr[4] = { 6, 0, 9}; //remaing would be 0
int arr[]{1,2,3};  //imp see above about this type of initialization
  • Arrays can also be passed as arguments to functions. When declaring the function, simply specify the array as a parameter, without a dimension. The array can then be used as normal within the function. For example:
#include <iostream>
using namespace std;

int sum(const int array[], const int length) {
 long sum = 0;
 for(int i = 0; i < length; sum += array[i++]);
 return sum;
 }

 int main() {
 int arr[] = {1, 2, 3, 4, 5, 6, 7};
 cout << "Sum: " << sum(arr, 7) << endl;
 return 0;
 }

The function sum takes a constant integer array and a constant integer length as its arguments and adds up length elements in the array. It then returns the sum, and the program prints out Sum: 28.

It is important to note that arrays are passed by reference and so any changes made to the array within the function will be observed in the calling scope.

multidimensional arrays

int twoDimArray[2][4] = { 6, 0, 9, 6, 2, 0, 1, 1 };
int twoDimArray[2][4] = { { 6, 0, 9, 6 } , { 2, 0, 1, 1 } };

Note that dimensions must always be provided when initializing multidimensional arrays, as it is otherwise impossible for the compiler to determine what the intended element partitioning is. For the same reason, when multidimensional arrays are specified as arguments to functions, all dimensions but the first must be provided (the first dimension is optional), as in the following:

int aFunction(int arr[][4]) { … }

Multidimensional arrays are merely an abstraction for programmers, as all of the elements in the array are sequential in memory. Declaring int arr[2][4]; is the same thing as declaring int arr[8];.

pointers and references

Variables and Memory

When you declare a variable, the computer associates the variable name with a particular location in memory and stores a value there.
When you refer to the variable by name in your code, the computer must take two steps:

  1. Look up the address that the variable name corresponds to
  2. Go to that location in memory and retrieve or set the value it contains

C++ allows us to perform either one of these steps independently on a variable with the & and * operators:

  1. &x evaluates to the address of x in memory.
  2. *( &x ) takes the address of x and dereferences it – it retrieves the value at that location in memory. *( &x ) thus evaluates to the same thing as x.

Motivating Pointers

Memory addresses, or pointers, allow us to manipulate data much more flexibly; manipulating the memory addresses of data can be more efficient than manipulating the data itself. Just a taste of what we’ll be able to do with pointers:

  • More flexible pass-by-reference
  • Manipulate complex data structures efficiently, even if their data is scattered in different memory locations
  • Use polymorphism – calling functions on data without knowing exactly what kind of data it is

The Nature of Pointers

Pointers are just variables storing integers – but those integers happen to be memory addresses, usually addresses of other variables. A pointer that stores the address of some variable x is said to point to x. We can access the value of x by dereferencing the pointer.

As with arrays, it is often helpful to visualize pointers by using a row of adjacent cells to represent memory locations, as below. Each cell represents 1 block of memory. The dot-arrow notation indicates that ptr “points to” x – that is, the value stored in ptr is 12314, x’s memory address. pointers

Declaring Pointers

  • To declare a pointer variable named ptr that points to an integer variable named x:
int *ptr = &x;

int *ptr declares the pointer to an integer value, which we are initializing to the address of x. We can have pointers to values of any type. The general scheme for declaring pointers is:

data_type *pointer_name; // Add "= initial_value " if applicable

pointer_name is then a variable of type data_type * – a “pointer to a data_type value.”

Using Pointer Values

Once a pointer is declared, we can dereference it with the * operator to access its value:

cout << *ptr; // Prints the value pointed to by ptr,
              // which in the above example would be x’s value

We can use deferenced pointers as l-values:

*ptr = 5; // Sets the value of x

Note that Without the * operator, the identifier x refers to the pointer itself, not the value it points to:

cout << ptr; // Outputs the memory address of x in base 16

Just like any other data type, we can pass pointers as arguments to functions. The same way we’d say void func(int x) {...}, we can say void func(int *x){...}. Here is an example of using pointers to square a number in a similar fashion to pass-by-reference:

void squareByPtr ( int* numPtr)
{
  //sth
}

const Pointers (important):boom:

There are two places the const keyword can be placed within a pointer variable declaration. This is because there are two different variables whose values you might want to forbid changing: the pointer itself and the value it points to.

const int *ptr;  //Note you can left it uninitialized becoz ptr is variable.
                //It can be changed. Here only location is read-only. When you say,
                // read-only location, it is with respect to pointer ptr. Content of
                //this location can be changed via other pointers
  • Remember Right-left Rule- :"it declares a changeable pointer to a constant integer".
  • Note that the integer value can't be changed through this pointers but the pointer may be changed to point to a different integer.
  • (const int )--> read-only location
int * const ptr; // Note you can't left this uninitialized
//because ptr is read-only varaible so you should assign it at
//the very first time. Here the location is not read-only.
//YOu can change the value in that location.
  • declares a constant pointer to changeable integer data. The integer value can be changed through this pointer, but the pointer may not be changed to point to a different constant integer.
  • ptr is read-only varible
const int * const ptr;   //error, you can't left it uninitialized
  • forbids changing either the address ptr contains or the value it points to.
example1 (location const)
/* Find out what will happen in this code? */

#include<iostream>
using namespace std;
int main() {
  int x = 5;
  int y = 10;

const  int  *i; //Now location is const with respect to the pointer i
i = &x;
(*i)++;  //fails, becoz location is const wrt i

int *j = i; // what is wrong here?? Invalid conversion from const int to int
(*j)++; // if line 13 had been ok, this line will be ok,, since line 13 says location pointed by j is not const
const  int *j = i;  // this is ok,,

 *i=*i+1;  //ERROR: assignment of read-only location (const int)
 i = &y; //pointer variable i is not const. you can make it point to others

  x++;
 cout<<*i<<"\n";
 cout<<*j;
  return 0;

}

example2 (location constant and also pointer constant)

/* Find out what will happen in this code? */

#include<iostream>
using namespace std;
int main() {
  int x = 5;
  int y = 10;

const  int *const i = &x;  //you need to assign such statement. It will throw error if you don't assign

 *i=*i+1;  //ERROR: assignment of read-only location (const int)
  i = &y; // ERROR: assignment of read-only variable i in i=&y

  x++;    // nobody is preventing you to change the value of x; its valid
 cout<<*i;  // what will it print?? 5 or 6?
  return 0;

}

example3 (pointer is constant)

#include<iostream>
using namespace std;
int main() {
  int x = 5;
  int y = 10;

  int *const i = &x;  //you need to assign such statement. It will throw error if you don't assign

  *i=*i+1;  //valid, because value pointing to can be changed but pointer itself can't be changed to point to other variable
  i = &y; // error, i is the read only variable. You can't reassign it

  x++;
 cout<<*i;  //what will it print?
  return 0;

}

example4 (location is Constant)

/* Find out what will happen in this code? */

#include<iostream>
using namespace std;
int main() {
  int x = 5;
  int y = 10;

  const int *i;   // this and the below are same
//  int  const *i ;  //this and the above one are same....Pls don't confuse on this one, remove one of them
  i = &x;     //i is non-const variable. you can assign it as many times as you want
 *i=*i+1;    //ERROR: you can't do this because location is const
  i = &y;     //Can you point to any other variable; Now qustion is can you change the new location value?
  (*i)++;    //ERROR: increment of read-only location i
  x++;
  cout<<*i;
  return 0;

}

I have tested most of the combinations. Pls see const_pointer for more details.

Null, Uninitialized, and Deallocated Pointers

Some pointers do not point to valid data; dereferencing such a pointer is a runtime error. Any pointerset to 0 is called a null pointer, and since there is no memory location 0, it is an invalid pointer. One should generally check whether a pointer is null before dereferencing it. Pointers are often set to 0 to signal that they are not currently valid.

Dereferencing pointers to data that has been erased from memory also usually causes runtime errors. 💥

int *myFunc(){
  int phantom = 4;
  return &phantom;
}

phantom is deallocated when myFunc exits, so the pointer the function returns is invalid.

Note: read/write pointer can't point to the read only location 💥 👊

int i = 2; //initialization is optional
const int ci = 3; // initialization required
++ci;// not ok
int *p = &i; //ok
++*p;
p = &ci; //not ok
++*p;

const int * pc; // ok; read-only view of the poiner I am pointing to
const int * pc = &ci;  //OK
++*pc; //not ok
++pc; // ok

pc = &i; //ok
int *imp = pc; //not ok becoz pc is read-only pointer(r-value), so its illegal




int * const cp; // not ok because poner can't move, it needs to be initialized
int *const cp = &ci; //not ok becoz ci is ref to const int. you can't assign read-only view pointer to pointer having read-write view
++*cp; //ok
++cp; //not ok

Use of const pointer

int * p = new int[1000];
...
...
++*p;   // but if you did ++p instead of ++*p by mistake, the program will crash
...
delete[] p;

Instead, if we did,

int *const p = new int [1000];
...
...
++p; //if you did this my mistake -> compiler will throw error and you can catch it befoer program will crash
...
delete [] p;

References

When we write void f(int &x) {...} and call f(y), the reference variable x becomes another name – an alias – for the value of y in memory. We can declare a reference variable locally, as well:

int y;
int &x = y; // Makes x a reference to, or alias of, y

After these declarations,changing x will change y and vice versa,because they are two names for the same thing. References are just pointers that are dereferenced every time they are used. Just like pointers, you can pass them around, return them, set other references to them, etc. The only differences between using pointers and using references are: 👊 🍁

  • References are sort of pre-dereferenced – you do not dereference them explicitly.
  • You cannot change the location to which a reference points, whereas you can change the location to which a pointer points. Because of this, references must always be initialized when they are declared.💖
  • When writing the value that you want to make a reference to, you do not put an & before it to take its address, whereas you do need to do this for pointers.
  • Reference should always be stuck to some target.
const int ci = 3;  //initialization required
int &c = ci;
int &ci; //ERROR:
const int &rc = ci;

Here reference rc is inherently constant. and note that the const in const int &rc = ci; is for the int.

int a [] = {2,3,4};
//a is stuck to  pointer
//same as ref, so it is tuck to a location
// same as int * const a;

Pointers and Arrays

The name of an array is actually a pointer to the first element in the array. Writing myArray[3] tells the compiler to return the element that is 3 away from the starting element of myArray.

This explains why arrays are always passed by reference: passing an array is really passing a pointer.

This also explains why array indices start at 0: the first element of an array is the element that is 0 away from the start of the array.

Pointer Arithmetic

Pointer arithmetic is a way of using subtraction and addition of pointers to move around between locations in memory, typically between array elements. Adding an integer n to a pointer produces a new pointer pointing to n positions further down in memory.

Array Access Notations

myArray[3] is *(myArray + 3)

char * Strings

  • a string is actually an array of characters.
  • When you set achar *to a string, you are really setting a pointer to point to the first character in the array that holds the string
  • You cannot modify string literals;, 👊 to do so is either a syntax error or a runtime error, depending on how you try to do it
  • String literals are loaded into read-only program memory at program startup. 💥
  • You can, however, modify the contents of an array of characters. Consider the following example
char courseName1[] = {’6’, ’.’, ’0’, ’9’, ’6’, ’\0 ’ };
char *courseName2 = "6.096 ";
  • Attempting to modify one of the elements courseName1 is permitted,
  • attempting to modify one of the characters in courseName2 will generate a runtime error, causing the program to crash.

structure and copy constructor

  • Why same name is getting printed for nodes as "Wendy"?
  // Example program
  #include <iostream>
  #include <string.h>


  using namespace std;

  struct Node {
      char *name;
      int age;
      Node(char *n = "", int a = 0){
          name = strdup(n);
          age = a;
      }
  };

  int main(){
      Node node1("Roger", 20), node2(node1);
      cout<<node1.name<<" "<<node1.age<<"\n"<<endl;
      cout<<node2.name<<" "<<node2.age<<"\n"<<endl;
      strcpy(node2.name,"Wendy");
      node2.age = 30;
      cout<<node1.name<<" "<<node1.age<<"\n"<<endl;
      cout<<node2.name<<" "<<node2.age<<"\n"<<endl;
  }
  /*Outputs
  Roger 20

  Roger 20

  Wendy 20

  Wendy 30 */
  • The problem is that the definition of Node does not provide a copy constructor

    Node(const Node&);

    which is necessary to execute the declaration node2(node1) to initialize node1.

  • If a user copy constructor is missing, the constructor is generated automatically by the compiler. But the compiler-generated copy constructor performs member-by-member copying. 🌹 🌺

  • Because name is a pointer, the copy constructor copies the string address node1.name to node2.name, not the string content.

  • To prevent this from happening, the user must define a proper copy constructor, as in

    struct Node {
        char *name;
        int age;
        Node(char *n = 0, int a = 0) {
            name = strdup(n);
            age = a;
        }
        Node(const Node& n) { // copy constructor;
            name = strdup(n.name);
            age = n.age;
        }
    };
  • Note that a similar problem is raised by the assignment operator. If a definition of the assignment operator is not provided by the user, an assignment.

    node1 = node2;

performs member-by-member copying, which leads to the same problem

  • To avoid the problem, the assignment operator has to be overloaded by the user. For Node, the overloading is accomplished by
    Node& operator=(const Node& n) {
      if (this != &n) { // no assignment to itself;
        if (name != 0)
          free(name);
        name = strdup(n.name);
        age = n.age;
      }
      return *this;
    }

In this code, a special pointer this is used. Each object can access its own address through the pointer this so that *this is the object itself.

⚠️ **GitHub.com Fallback** ⚠️